Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2022-09-28 12:16:06 +02:00
commit 7303d311d5
283 changed files with 1548 additions and 1252 deletions

View file

@ -2,8 +2,8 @@
{promesa.core/let clojure.core/let {promesa.core/let clojure.core/let
promesa.core/->> clojure.core/->> promesa.core/->> clojure.core/->>
promesa.core/-> clojure.core/-> promesa.core/-> clojure.core/->
rumext.alpha/defc clojure.core/defn rumext.v2/defc clojure.core/defn
rumext.alpha/fnc clojure.core/fn rumext.v2/fnc clojure.core/fn
app.common.data/export clojure.core/def app.common.data/export clojure.core/def
app.db/with-atomic clojure.core/with-open app.db/with-atomic clojure.core/with-open
app.common.data.macros/get-in clojure.core/get-in app.common.data.macros/get-in clojure.core/get-in

View file

@ -1,6 +1,6 @@
# CHANGELOG # CHANGELOG
## :rocket: Next ## :rocket: 1.16.0-beta
### :boom: Breaking changes & Deprecations ### :boom: Breaking changes & Deprecations
### :sparkles: New features ### :sparkles: New features
@ -47,6 +47,7 @@
- Fix inconsistent message on deleting library when a library is linked from deleted files - 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 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 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 ### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!) ### :heart: Community contributions by (Thank you!)
@ -78,6 +79,7 @@
- Fix Terms and Privacy links overlapping [Taiga #4137](https://tree.taiga.io/project/penpot/issue/4137) - 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 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 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 ## 1.15.3-beta

View file

@ -6,26 +6,26 @@
;; Logging ;; Logging
org.zeromq/jeromq {:mvn/version "0.5.2"} 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"} org.clojure/data.fressian {:mvn/version "1.0.0"}
io.prometheus/simpleclient {:mvn/version "0.15.0"} io.prometheus/simpleclient {:mvn/version "0.16.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.15.0"} io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
io.prometheus/simpleclient_jetty {:mvn/version "0.15.0" io.prometheus/simpleclient_jetty {:mvn/version "0.16.0"
:exclusions [org.eclipse.jetty/jetty-server :exclusions [org.eclipse.jetty/jetty-server
org.eclipse.jetty/jetty-servlet]} 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"} java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/yetti {:git/tag "v9.8" :git/sha "fbe1d7d" funcool/yetti {:git/tag "v9.8" :git/sha "fbe1d7d"
:git/url "https://github.com/funcool/yetti.git" :git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]} :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"} 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"} com.zaxxer/HikariCP {:mvn/version "5.0.1"}
io.whitfin/siphash {:mvn/version "2.0.0"} io.whitfin/siphash {:mvn/version "2.0.0"}
@ -42,14 +42,12 @@
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"} org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
integrant/integrant {:mvn/version "0.8.0"} integrant/integrant {:mvn/version "0.8.0"}
io.sentry/sentry {:mvn/version "5.6.1"}
dawran6/emoji {:mvn/version "0.1.5"} 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 Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"} 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"] :paths ["src" "resources" "target/classes"]
:aliases :aliases
@ -65,8 +63,7 @@
:extra-paths ["test" "dev"]} :extra-paths ["test" "dev"]}
:build :build
{:extra-deps {:extra-deps {io.github.clojure/tools.build {:git/tag "v0.8.3" :git/sha "0d20256"}}
{io.github.clojure/tools.build {:git/tag "v0.8.2" :git/sha "ba1a2bf"}}
:ns-default build} :ns-default build}
:test :test

View file

@ -434,6 +434,10 @@
(assoc :path "/#/auth/verify-token") (assoc :path "/#/auth/verify-token")
(assoc :query (u/map->query-string params)))] (assoc :query (u/map->query-string params)))]
(when (:is-blocked profile)
(ex/raise :type :restriction
:code :profile-blocked))
(when (fn? audit) (when (fn? audit)
(audit :cmd :submit (audit :cmd :submit
:type "command" :type "command"

View file

@ -198,11 +198,6 @@
(s/def ::telemetry-with-taiga ::us/boolean) (s/def ::telemetry-with-taiga ::us/boolean)
(s/def ::tenant ::us/string) (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/def ::config
(s/keys :opt-un [::secret-key (s/keys :opt-un [::secret-key
::flags ::flags
@ -276,17 +271,13 @@
::public-uri ::public-uri
::redis-uri ::redis-uri
::registration-domain-whitelist ::registration-domain-whitelist
::rpc-rlimit-config
::semaphore-process-font ::semaphore-process-font
::semaphore-process-image ::semaphore-process-image
::semaphore-update-file ::semaphore-update-file
::semaphore-auth ::semaphore-auth
::rpc-rlimit-config
::sentry-dsn
::sentry-debug
::sentry-attach-stack-trace
::sentry-trace-sample-rate
::smtp-default-from ::smtp-default-from
::smtp-default-reply-to ::smtp-default-reply-to
::smtp-host ::smtp-host
@ -295,8 +286,10 @@
::smtp-ssl ::smtp-ssl
::smtp-tls ::smtp-tls
::smtp-username ::smtp-username
::srepl-host ::srepl-host
::srepl-port ::srepl-port
::assets-storage-backend ::assets-storage-backend
::storage-assets-fs-directory ::storage-assets-fs-directory
::storage-assets-s3-bucket ::storage-assets-s3-bucket

View file

@ -5,6 +5,7 @@
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.db (ns app.db
(:refer-clojure :exclude [get])
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
@ -270,28 +271,55 @@
(sql/delete table params opts) (sql/delete table params opts)
(assoc opts :return-keys true)))) (assoc opts :return-keys true))))
(defn- is-deleted? (defn is-row-deleted?
[{:keys [deleted-at]}] [{:keys [deleted-at]}]
(and (dt/instant? deleted-at) (and (dt/instant? deleted-at)
(< (inst-ms deleted-at) (< (inst-ms deleted-at)
(inst-ms (dt/now))))) (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] ([ds table params]
(get-by-params ds table params nil)) (get* ds table params nil))
([ds table params {:keys [check-not-found] :or {check-not-found true} :as opts}] ([ds table params {:keys [check-deleted?] :or {check-deleted? true} :as opts}]
(let [res (exec-one! ds (sql/select table params opts))] (let [rows (exec! ds (sql/select table params opts))
(when (and check-not-found (or (not res) (is-deleted? res))) 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 (ex/raise :type :not-found
:table table :table table
:hint "database object not found")) :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 (defn get-by-id
([ds table id] ([ds table id]
(get-by-params ds table {:id id} nil)) (get ds table {:id id} nil))
([ds table id opts] ([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 (defn query
([ds table params] ([ds table params]

View file

@ -56,7 +56,6 @@
type (resolve-recipient-type type)] type (resolve-recipient-type type)]
(.addRecipients mmsg type address) (.addRecipients mmsg type address)
mmsg))) mmsg)))
(defn- assign-recipients (defn- assign-recipients
[mmsg {:keys [to cc bcc] :as params}] [mmsg {:keys [to cc bcc] :as params}]
(cond-> mmsg (cond-> mmsg
@ -139,6 +138,7 @@
(Properties.) (Properties.)
{"mail.user" username {"mail.user" username
"mail.host" host "mail.host" host
"mail.debug" (contains? cf/flags :smtp-debug)
"mail.from" default-from "mail.from" default-from
"mail.smtp.auth" (boolean username) "mail.smtp.auth" (boolean username)
"mail.smtp.starttls.enable" tls "mail.smtp.starttls.enable" tls
@ -150,17 +150,14 @@
"mail.smtp.connectiontimeout" timeout})) "mail.smtp.connectiontimeout" timeout}))
(defn- create-smtp-session (defn- create-smtp-session
[{:keys [debug] :or {debug false} :as opts}] [opts]
(let [props (opts->props opts) (let [props (opts->props opts)]
session (Session/getInstance props)] (Session/getInstance props)))
(.setDebug session debug)
session))
(defn- create-smtp-message (defn- create-smtp-message
^MimeMessage ^MimeMessage
[cfg params] [cfg session params]
(let [session (create-smtp-session cfg) (let [mmsg (MimeMessage. ^Session session)]
mmsg (MimeMessage. ^Session session)]
(assign-recipients mmsg params) (assign-recipients mmsg params)
(assign-from mmsg cfg params) (assign-from mmsg cfg params)
(assign-reply-to mmsg cfg params) (assign-reply-to mmsg cfg params)
@ -304,9 +301,16 @@
[_ cfg] [_ cfg]
(fn [params] (fn [params]
(when (contains? cf/flags :smtp) (when (contains? cf/flags :smtp)
(Transport/send (create-smtp-message cfg params) (let [session (create-smtp-session cfg)]
(:username cfg) (with-open [transport (.getTransport session (if (:ssl cfg) "smtps" "smtp"))]
(:password cfg))) (.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) (when (or (contains? cf/flags :log-emails)
(not (contains? cf/flags :smtp))) (not (contains? cf/flags :smtp)))

View file

@ -52,13 +52,6 @@
(let [mdata (meta obj) (let [mdata (meta obj)
backend (sto/resolve-backend storage (:backend obj))] backend (sto/resolve-backend storage (:backend obj))]
(case (:type backend) (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 :s3
(p/let [{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age signature-max-age})] (p/let [{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age signature-max-age})]
(yrs/response :status 307 (yrs/response :status 307

View file

@ -8,6 +8,7 @@
(:refer-clojure :exclude [error-handler]) (:refer-clojure :exclude [error-handler])
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.pprint :as pp] [app.common.pprint :as pp]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
@ -213,7 +214,7 @@
(render-template [report] (render-template [report]
(let [context (dissoc 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) :spec-explain :spec-value :error :explain :hint)
params {:context (pp/pprint-str context :width 200) params {:context (pp/pprint-str context :width 200)
:hint (:hint report) :hint (:hint report)
@ -341,8 +342,13 @@
"Mainly a task that performs a health check." "Mainly a task that performs a health check."
[{:keys [pool]} _] [{:keys [pool]} _]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(try
(db/exec-one! conn ["select count(*) as count from server_prop;"]) (db/exec-one! conn ["select count(*) as count from server_prop;"])
(yrs/response 200 "OK"))) (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 (defn changelog-handler
[_ _] [_ _]

View file

@ -46,6 +46,7 @@
(defn parse-event (defn parse-event
[event] [event]
(-> (parse-event-data event) (-> (parse-event-data event)
(assoc :hint (or (:hint event) (:message event)))
(assoc :tenant (cf/get :tenant)) (assoc :tenant (cf/get :tenant))
(assoc :host (cf/get :host)) (assoc :host (cf/get :host))
(assoc :public-uri (cf/get :public-uri)) (assoc :public-uri (cf/get :public-uri))

View file

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

View file

@ -244,6 +244,9 @@
{:name "0078-mod-file-media-object-table-drop-cascade" {:name "0078-mod-file-media-object-table-drop-cascade"
:fn (mg/resource "app/migrations/sql/0078-mod-file-media-object-table-drop-cascade.sql")} :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")}
]) ])

View file

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

View file

@ -31,9 +31,10 @@
(defn- handle-response-transformation (defn- handle-response-transformation
[response request mdata] [response request mdata]
(let [response (if (sv/wrapped? response) @response response)]
(if-let [transform-fn (:transform-response mdata)] (if-let [transform-fn (:transform-response mdata)]
(p/do (transform-fn request response)) (p/do (transform-fn request response))
(p/resolved response))) (p/resolved response))))
(defn- handle-before-comple-hook (defn- handle-before-comple-hook
[response mdata] [response mdata]
@ -222,6 +223,7 @@
(->> (sv/scan-ns 'app.rpc.commands.binfile (->> (sv/scan-ns 'app.rpc.commands.binfile
'app.rpc.commands.comments 'app.rpc.commands.comments
'app.rpc.commands.management 'app.rpc.commands.management
'app.rpc.commands.verify-token
'app.rpc.commands.auth 'app.rpc.commands.auth
'app.rpc.commands.ldap 'app.rpc.commands.ldap
'app.rpc.commands.demo 'app.rpc.commands.demo

View file

@ -6,6 +6,7 @@
(ns app.rpc.commands.auth (ns app.rpc.commands.auth
(:require (:require
[app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
@ -96,15 +97,19 @@
(:valid (verify-password password (:password profile)))) (:valid (verify-password password (:password profile))))
(validate-profile [profile] (validate-profile [profile]
(when-not (:is-active profile)
(ex/raise :type :validation
:code :wrong-credentials))
(when-not profile (when-not profile
(ex/raise :type :validation (ex/raise :type :validation
:code :wrong-credentials)) :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) (when-not (check-password profile password)
(ex/raise :type :validation (ex/raise :type :validation
:code :wrong-credentials)) :code :wrong-credentials))
profile)] profile)]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
@ -184,8 +189,9 @@
;; ---- COMMAND: Prepare Register ;; ---- COMMAND: Prepare Register
(defn prepare-register (defn validate-register-attempt!
[{:keys [pool sprops] :as cfg} params] [{:keys [pool sprops]} params]
(when-not (contains? cf/flags :registration) (when-not (contains? cf/flags :registration)
(if-not (contains? params :invitation-token) (if-not (contains? params :invitation-token)
(ex/raise :type :restriction (ex/raise :type :restriction
@ -208,20 +214,50 @@
:code :email-has-permanent-bounces :code :email-has-permanent-bounces
:hint "looks like the email has one or many bounces reported")) :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)) (when (= (str/lower (:email params))
(str/lower (:password params))) (str/lower (:password params)))
(ex/raise :type :validation (ex/raise :type :validation
:code :email-as-password :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) (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) :password (:password params)
:invitation-token (:invitation-token params) :invitation-token (:invitation-token params)
:backend "penpot" :backend "penpot"
:iss :prepared-register :iss :prepared-register
:exp (dt/in-future "48h")} :profile-id (:id profile)
:exp (dt/in-future {:days 7})}
params (d/without-nils params)
token (tokens/generate sprops params)] token (tokens/generate sprops params)]
(with-meta {:token token} (with-meta {:token token}
@ -240,11 +276,10 @@
;; ---- COMMAND: Register Profile ;; ---- COMMAND: Register Profile
(defn create-profile (defn create-profile
"Create the profile entry on the database with limited input filling "Create the profile entry on the database with limited set of input
all the other fields with defaults." attrs (all the other attrs are filled with default values)."
[conn params] [conn params]
(let [id (or (:id params) (uuid/next)) (let [id (or (:id params) (uuid/next))
props (-> (audit/extract-utm-params params) props (-> (audit/extract-utm-params params)
(merge (:props params)) (merge (:props params))
(merge {:viewed-tutorial? false (merge {:viewed-tutorial? false
@ -321,21 +356,35 @@
(defn register-profile (defn register-profile
[{:keys [conn sprops session] :as cfg} {:keys [token] :as params}] [{:keys [conn sprops session] :as cfg} {:keys [token] :as params}]
(let [claims (tokens/verify sprops {:token token :iss :prepared-register}) (let [claims (tokens/verify sprops {:token token :iss :prepared-register})
params (merge params claims)] params (merge params claims)
(check-profile-existence! conn params)
(let [is-active (or (:is-active params) is-active (or (:is-active params)
(not (contains? cf/flags :email-verification)) (not (contains? cf/flags :email-verification))
;; DEPRECATED: v1.15 ;; DEPRECATED: v1.15
(contains? cf/flags :insecure-register)) (contains? cf/flags :insecure-register))
profile (->> (assoc params :is-active is-active) 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 conn)
(create-profile-relations conn) (create-profile-relations conn)
(profile/decode-profile-row)) (profile/decode-profile-row)))
audit-fn (:audit cfg)
invitation (when-let [token (:invitation-token params)] invitation (when-let [token (:invitation-token params)]
(tokens/verify sprops {:token token :iss :team-invitation}))] (tokens/verify sprops {:token token :iss :team-invitation}))]
;; If profile is filled in claims, means it tries to register
;; again, so we proceed to update the modified-at attr
;; 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 (cond
;; If invitation token comes in params, this is because the ;; If invitation token comes in params, this is because the
;; user comes from team-invitation process; in this case, ;; user comes from team-invitation process; in this case,
@ -375,7 +424,7 @@
(send-email-verification! conn sprops profile) (send-email-verification! conn sprops profile)
(with-meta profile (with-meta profile
{::audit/replace-props (audit/profile->props profile) {::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})))))) ::audit/profile-id (:id profile)})))))
(s/def ::register-profile (s/def ::register-profile
(s/keys :req-un [::token ::fullname])) (s/keys :req-un [::token ::fullname]))

View file

@ -46,6 +46,11 @@
:code :wrong-credentials)) :code :wrong-credentials))
(let [profile (login-or-register cfg info)] (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-let [token (:invitation-token params)]
;; If invitation token comes in params, this is because the ;; If invitation token comes in params, this is because the
;; user comes from team-invitation process; in this case, ;; user comes from team-invitation process; in this case,

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

View file

@ -565,6 +565,8 @@
(s/keys :req-un [::profile-id ::file-id ::revn ::data ::props])) (s/keys :req-un [::profile-id ::file-id ::revn ::data ::props]))
(sv/defmethod ::upsert-file-thumbnail (sv/defmethod ::upsert-file-thumbnail
"Creates or updates the file thumbnail. Mainly used for paint the
grid thumbnals."
[{:keys [pool] :as cfg} {:keys [profile-id file-id revn data props]}] [{:keys [pool] :as cfg} {:keys [profile-id file-id revn data props]}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id) (files/check-edition-permissions! conn profile-id file-id)

View file

@ -252,6 +252,7 @@
;; --- MUTATION: Delete Profile ;; --- MUTATION: Delete Profile
(declare get-owned-teams-with-participants)
(declare check-can-delete-profile!) (declare check-can-delete-profile!)
(declare mark-profile-as-deleted!) (declare mark-profile-as-deleted!)
@ -261,14 +262,29 @@
(sv/defmethod ::delete-profile (sv/defmethod ::delete-profile
[{:keys [pool session] :as cfg} {:keys [profile-id] :as params}] [{:keys [pool session] :as cfg} {:keys [profile-id] :as params}]
(db/with-atomic [conn pool] (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)]
;; If we found owned teams with participants, we don't allow
;; delete profile until the user properly transfer ownership or
;; explicitly removes all participants from the team
(when (some pos? (map :participants teams))
(ex/raise :type :validation
:code :owner-teams-with-people
:hint "The user need to transfer ownership of owned teams."
:context {:teams (mapv :id teams)}))
(doseq [{:keys [id]} teams]
(db/update! conn :team
{:deleted-at deleted-at}
{:id id}))
(db/update! conn :profile (db/update! conn :profile
{:deleted-at (dt/now)} {:deleted-at deleted-at}
{:id profile-id}) {:id profile-id})
(with-meta {} (with-meta {}
{:transform-response (:delete session)}))) {:transform-response (:delete session)}))))
(def sql:owned-teams (def sql:owned-teams
"with owner_teams as ( "with owner_teams as (
@ -277,23 +293,16 @@
where tpr.is_owner is true where tpr.is_owner is true
and tpr.profile_id = ? and tpr.profile_id = ?
) )
select tpr.team_id, select tpr.team_id as id,
count(tpr.profile_id) as num_profiles count(tpr.profile_id) - 1 as participants
from team_profile_rel as tpr from team_profile_rel as tpr
where tpr.team_id in (select id from owner_teams) where tpr.team_id in (select id from owner_teams)
and tpr.profile_id != ?
group by 1") group by 1")
(defn- check-can-delete-profile! (defn- get-owned-teams-with-participants
[conn profile-id] [conn profile-id]
(let [rows (db/exec! conn [sql:owned-teams profile-id])] (db/exec! conn [sql:owned-teams profile-id 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)}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; DEPRECATED METHODS (TO BE REMOVED ON 1.16.x) ;; DEPRECATED METHODS (TO BE REMOVED ON 1.16.x)

View file

@ -376,18 +376,17 @@
:code :profile-is-muted :code :profile-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces")) :hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
(doseq [email emails] (let [invitations (->> emails
(create-team-invitation (map (fn [email]
(assoc cfg (assoc cfg
:email email :email email
:conn conn :conn conn
:team team :team team
:profile profile :profile profile
:role role)) :role role)))
) (map create-team-invitation))]
(with-meta (vec invitations)
(with-meta {} {::audit/props {:invitations (count invitations)}})))))
{::audit/props {:invitations (count emails)}}))))
(def sql:upsert-team-invitation (def sql:upsert-team-invitation
"insert into team_invitation(team_id, email_to, role, valid_until) "insert into team_invitation(team_id, email_to, role, valid_until)
@ -399,6 +398,7 @@
[{:keys [conn sprops team profile role email] :as cfg}] [{:keys [conn sprops team profile role email] :as cfg}]
(let [member (profile/retrieve-profile-data-by-email conn email) (let [member (profile/retrieve-profile-data-by-email conn email)
token-exp (dt/in-future "168h") ;; 7 days token-exp (dt/in-future "168h") ;; 7 days
email (str/lower email)
itoken (tokens/generate sprops itoken (tokens/generate sprops
{:iss :team-invitation {:iss :team-invitation
:exp token-exp :exp token-exp
@ -412,9 +412,6 @@
:profile-id (:id profile) :profile-id (:id profile)
:exp (dt/in-future {:days 30})})] :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))) (when (and member (not (eml/allow-send-emails? conn member)))
(ex/raise :type :validation (ex/raise :type :validation
:code :member-is-muted :code :member-is-muted
@ -428,6 +425,9 @@
:email email :email email
:hint "the email you invite has been repeatedly reported as spam or bounce")) :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 ;; When we have email verification disabled and invitation user is
;; already present in the database, we proceed to add it to the ;; already present in the database, we proceed to add it to the
;; team as-is, without email roundtrip. ;; team as-is, without email roundtrip.
@ -448,10 +448,7 @@
(when-not (:is-active member) (when-not (:is-active member)
(db/update! conn :profile (db/update! conn :profile
{:is-active true} {:is-active true}
{:id (:id member)})) {:id (:id member)})))
(assoc member :is-active true))
(do (do
(db/exec-one! conn [sql:upsert-team-invitation (db/exec-one! conn [sql:upsert-team-invitation
(:id team) (str/lower email) (name role) (:id team) (str/lower email) (name role)
@ -463,7 +460,9 @@
:invited-by (:fullname profile) :invited-by (:fullname profile)
:team (:name team) :team (:name team)
:token itoken :token itoken
:extra-data ptoken}))))) :extra-data ptoken})))
itoken))
;; --- Mutation: Create Team & Invite Members ;; --- Mutation: Create Team & Invite Members

View file

@ -6,170 +6,23 @@
(ns app.rpc.mutations.verify-token (ns app.rpc.mutations.verify-token
(:require (:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.db :as db] [app.db :as db]
[app.loggers.audit :as audit] [app.rpc.commands.verify-token :refer [process-token]]
[app.rpc.mutations.teams :as teams] [app.rpc.doc :as-alias doc]
[app.rpc.queries.profile :as profile]
[app.tokens :as tokens] [app.tokens :as tokens]
[app.tokens.spec.team-invitation :as-alias spec.team-invitation]
[app.util.services :as sv] [app.util.services :as sv]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]))
[cuerdas.core :as str]))
(defmulti process-token (fn [_ _ claims] (:iss claims)))
(s/def ::verify-token (s/def ::verify-token
(s/keys :req-un [::token] (s/keys :req-un [::token]
:opt-un [::profile-id])) :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}] [{:keys [pool sprops] :as cfg} {:keys [token] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(let [claims (tokens/verify sprops {:token token}) (let [claims (tokens/verify sprops {:token token})
cfg (assoc cfg :conn conn)] cfg (assoc cfg :conn conn)]
(process-token cfg params claims)))) (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))

View file

@ -62,9 +62,9 @@
(cmd.auth/send-email-verification! pool sprops profile) (cmd.auth/send-email-verification! pool sprops profile)
:email-sent)) :email-sent))
(defn update-profile (defn update-profile!
"Update a limited set of profile attrs." "Update a limited set of profile attrs."
[system & {:keys [email id active? deleted?]}] [system & {:keys [email id active? deleted? blocked?]}]
(us/verify! (us/verify!
:expr (some? system) :expr (some? system)
@ -74,15 +74,30 @@
:expr (or (string? email) (uuid? id)) :expr (or (string? email) (uuid? id))
:hint "email or id should be provided") :hint "email or id should be provided")
(let [pool (:app.db/pool system) (let [params (cond-> {}
params (cond-> {}
(true? active?) (assoc :is-active true) (true? active?) (assoc :is-active true)
(false? active?) (assoc :is-active false) (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-> {} opts (cond-> {}
(some? email) (assoc :email (str/lower email)) (some? email) (assoc :email (str/lower email))
(some? id) (assoc :id id))] (some? id) (assoc :id id))]
(some-> (db/update! pool :profile params opts) (db/with-atomic [conn (:app.db/pool system)]
(profile/decode-profile-row)))) (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))))

View file

@ -69,8 +69,7 @@
(defmethod delete-objects "team_font_variant" (defmethod delete-objects "team_font_variant"
[{:keys [conn min-age storage table] :as cfg}] [{:keys [conn min-age storage table] :as cfg}]
(let [sql (str/fmt sql:delete-objects (let [sql (str/fmt sql:delete-objects {:table table :limit 50})
{:table table :limit 50})
fonts (db/exec! conn [sql min-age]) fonts (db/exec! conn [sql min-age])
storage (media/configure-assets-storage storage conn)] storage (media/configure-assets-storage storage conn)]
(doseq [{:keys [id] :as font} fonts] (doseq [{:keys [id] :as font} fonts]
@ -85,10 +84,9 @@
(defmethod delete-objects "team" (defmethod delete-objects "team"
[{:keys [conn min-age storage table] :as cfg}] [{:keys [conn min-age storage table] :as cfg}]
(let [sql (str/fmt sql:delete-objects (let [sql (str/fmt sql:delete-objects {:table table :limit 50})
{:table table :limit 50})
teams (db/exec! conn [sql min-age]) 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] (doseq [{:keys [id] :as team} teams]
(l/debug :hint "permanently delete object" :table table :id id) (l/debug :hint "permanently delete object" :table table :id id)
@ -103,32 +101,17 @@
where deleted_at is not null where deleted_at is not null
and deleted_at < now() - ?::interval and deleted_at < now() - ?::interval
order by deleted_at order by deleted_at
limit %(limit)s limit ?
for update") 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" (defmethod delete-objects "profile"
[{:keys [conn min-age storage table] :as cfg}] [{:keys [conn min-age storage table] :as cfg}]
(let [sql (str/fmt sql:retrieve-deleted-profiles {:limit 50}) (let [profiles (db/exec! conn [sql:retrieve-deleted-profiles min-age 50])
profiles (db/exec! conn [sql min-age]) storage (media/configure-assets-storage storage conn)]
storage (assoc storage :conn conn)]
(doseq [{:keys [id] :as profile} profiles] (doseq [{:keys [id] :as profile} profiles]
(l/debug :hint "permanently delete object" :table table :id id) (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 ;; Mark as deleted the storage object related with the photo-id
;; field. ;; field.
(some->> (:photo-id profile) (sto/touch-object! storage) deref) (some->> (:photo-id profile) (sto/touch-object! storage) deref)
@ -164,10 +147,9 @@
(defmethod ig/init-key ::handler (defmethod ig/init-key ::handler
[_ {:keys [pool] :as cfg}] [_ {:keys [pool] :as cfg}]
(fn [params] (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] (db/with-atomic [conn pool]
(let [cfg (-> cfg (let [min-age (or (:min-age params) (:min-age cfg))
cfg (-> cfg
(assoc :min-age (db/interval min-age)) (assoc :min-age (db/interval min-age))
(assoc :conn conn))] (assoc :conn conn))]
(loop [tables (seq target-tables) (loop [tables (seq target-tables)
@ -176,10 +158,12 @@
(recur (rest tables) (recur (rest tables)
(+ total (process-table (assoc cfg :table table)))) (+ total (process-table (assoc cfg :table table))))
(do (do
(l/info :hint "task finished" :min-age (dt/format-duration min-age) :total total) (l/info :hint "objects gc finished succesfully"
:min-age (dt/format-duration min-age)
:total total)
(when (:rollback? params) (when (:rollback? params)
(db/rollback! conn)) (db/rollback! conn))
{:processed total})))))))) {:processed total})))))))

View file

@ -26,10 +26,14 @@
(t/encode))] (t/encode))]
(jwe/encrypt payload tokens-key {:alg :a256kw :enc :a256gcm}))) (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 (defn verify
[{:keys [tokens-key]} {:keys [token] :as params}] [sprops {:keys [token] :as params}]
(let [payload (jwe/decrypt token tokens-key {:alg :a256kw :enc :a256gcm}) (let [claims (decode sprops token)]
claims (t/decode payload)]
(when (and (dt/instant? (:exp claims)) (when (and (dt/instant? (:exp claims))
(dt/is-before? (:exp claims) (dt/now))) (dt/is-before? (:exp claims) (dt/now)))
(ex/raise :type :validation (ex/raise :type :validation

View file

@ -11,6 +11,20 @@
[app.common.data :as d] [app.common.data :as d]
[cuerdas.core :as str])) [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 (defmacro defmethod
[sname & body] [sname & body]
(let [[docs body] (if (string? (first body)) (let [[docs body] (if (string? (first body))

View file

@ -250,9 +250,10 @@
(t/deftest test-allow-send-messages-predicate-with-bounces (t/deftest test-allow-send-messages-predicate-with-bounces
(with-mocks [mock {:target 'app.config/get (with-mocks [mock {:target 'app.config/get
:return (th/mock-config-get-with :return (th/config-get-mock
{:profile-bounce-threshold 3 {:profile-bounce-threshold 3
:profile-complaint-threshold 2})}] :profile-complaint-threshold 2})}]
(let [profile (th/create-profile* 1) (let [profile (th/create-profile* 1)
pool (:app.db/pool th/*system*)] pool (:app.db/pool th/*system*)]
(th/create-complaint-for pool {:type :bounce :id (:id profile) :created-at (dt/in-past {:days 8})}) (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)}) (th/create-complaint-for pool {:type :bounce :id (:id profile)})
(t/is (true? (emails/allow-send-emails? pool 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)}) (th/create-complaint-for pool {:type :bounce :id (:id profile)})
(t/is (false? (emails/allow-send-emails? pool profile)))))) (t/is (false? (emails/allow-send-emails? pool profile))))))
@ -268,7 +269,7 @@
(t/deftest test-allow-send-messages-predicate-with-complaints (t/deftest test-allow-send-messages-predicate-with-complaints
(with-mocks [mock {:target 'app.config/get (with-mocks [mock {:target 'app.config/get
:return (th/mock-config-get-with :return (th/config-get-mock
{:profile-bounce-threshold 3 {:profile-bounce-threshold 3
:profile-complaint-threshold 2})}] :profile-complaint-threshold 2})}]
(let [profile (th/create-profile* 1) (let [profile (th/create-profile* 1)
@ -280,7 +281,7 @@
(th/create-complaint-for pool {:type :complaint :id (:id profile)}) (th/create-complaint-for pool {:type :complaint :id (:id profile)})
(t/is (true? (emails/allow-send-emails? pool 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)}) (th/create-complaint-for pool {:type :complaint :id (:id profile)})
(t/is (false? (emails/allow-send-emails? pool profile)))))) (t/is (false? (emails/allow-send-emails? pool profile))))))

View file

@ -537,10 +537,12 @@
:file-id (:id file) :file-id (:id file)
:object-id frame1-id :object-id frame1-id
:components-v2 true} :components-v2 true}
{:keys [error result] :as out} (th/query! data)] out (th/query! data)]
;; (th/print-result! out)
(t/is (= :validation (th/ex-type error))) (t/is (not (th/success? out)))
(t/is (= :spec-validation (th/ex-code error))))) (let [{:keys [type code]} (-> out :error ex-data)]
(t/is (= :validation type))
(t/is (= :spec-validation code)))))
(t/testing "RPC :file-data-for-thumbnail" (t/testing "RPC :file-data-for-thumbnail"
;; Insert a thumbnail data for the frame-id ;; Insert a thumbnail data for the frame-id
@ -728,8 +730,8 @@
;; Then query the specific revn ;; Then query the specific revn
(let [{:keys [result error] :as out} (th/query! (assoc data :revn 1))] (let [{:keys [result error] :as out} (th/query! (assoc data :revn 1))]
(t/is (= :not-found (th/ex-type error))) (t/is (th/ex-of-type? error :not-found))
(t/is (= :file-thumbnail-not-found (th/ex-code error))))) (t/is (th/ex-of-code? error :file-thumbnail-not-found))))
)) ))

View file

@ -119,15 +119,14 @@
)) ))
(t/deftest profile-deletion-simple (t/deftest profile-deletion-simple
(let [task (:app.tasks.objects-gc/handler th/*system*) (let [prof (th/create-profile* 1)
prof (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id prof) file (th/create-file* 1 {:profile-id (:id prof)
:project-id (:default-project-id prof) :project-id (:default-project-id prof)
:is-shared false})] :is-shared false})]
;; profile is not deleted because it does not meet all ;; profile is not deleted because it does not meet all
;; conditions to be deleted. ;; 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)))) (t/is (= 0 (:processed result))))
;; Request profile to be deleted ;; Request profile to be deleted
@ -146,7 +145,7 @@
(t/is (= 1 (count (:result out))))) (t/is (= 1 (count (:result out)))))
;; execute permanent deletion task ;; 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)))) (t/is (= 1 (:processed result))))
;; query profile after delete ;; query profile after delete
@ -166,7 +165,7 @@
(t/testing "not allowed email domain" (t/testing "not allowed email domain"
(t/is (false? (cauth/email-domain-in-whitelist? whitelist "username@somedomain.com")))))) (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 (let [data {::th/type :prepare-register-profile
:email "user@example.com" :email "user@example.com"
:password "foobar"} :password "foobar"}
@ -195,6 +194,100 @@
(t/is (nil? error)))) (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 (t/deftest prepare-and-register-with-invitation-and-disabled-registration-1
(with-redefs [app.config/flags [:disable-registration]] (with-redefs [app.config/flags [:disable-registration]]
(let [sprops (:app.setup/props th/*system*) (let [sprops (:app.setup/props th/*system*)
@ -239,34 +332,39 @@
:invitation-token itoken :invitation-token itoken
:email "user@example.com" :email "user@example.com"
:password "foobar"} :password "foobar"}
{:keys [result error] :as out} (th/mutation! data)] out (th/command! 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))))))
(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 (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 (let [data {::th/type :prepare-register-profile
:email "user@example.com" :email "user@example.com"
:password "foobar"}] :password "foobar"}
(let [{:keys [result error] :as out} (th/mutation! data)] out (th/command! data)]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :restriction)) (t/is (not (th/success? out)))
(t/is (th/ex-of-code? error :registration-disabled)))))) (let [edata (-> out :error ex-data)]
(t/is (= :restriction (:type edata)))
(t/is (= :registration-disabled (:code edata)))))))
(t/deftest prepare-register-with-existing-user (t/deftest prepare-register-with-existing-user
(let [profile (th/create-profile* 1) (let [profile (th/create-profile* 1)
data {::th/type :prepare-register-profile data {::th/type :prepare-register-profile
:email (:email profile) :email (:email profile)
:password "foobar"}] :password "foobar"}
(let [{:keys [result error] :as out} (th/mutation! data)] out (th/command! 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)))))
(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*) (let [pool (:app.db/pool th/*system*)
data {::th/type :prepare-register-profile data {::th/type :prepare-register-profile
:email "user@example.com" :email "user@example.com"
@ -274,34 +372,38 @@
(th/create-global-complaint-for pool {:type :bounce :email "user@example.com"}) (th/create-global-complaint-for pool {:type :bounce :email "user@example.com"})
(let [{:keys [result error] :as out} (th/mutation! data)] (let [out (th/command! data)]
(t/is (th/ex-info? error)) (t/is (not (th/success? out)))
(t/is (th/ex-of-type? error :validation)) (let [edata (-> out :error ex-data)]
(t/is (th/ex-of-code? error :email-has-permanent-bounces))))) (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*) (let [pool (:app.db/pool th/*system*)
data {::th/type :prepare-register-profile data {::th/type :prepare-register-profile
:email "user@example.com" :email "user@example.com"
:password "foobar"}] :password "foobar"}]
(th/create-global-complaint-for pool {:type :complaint :email "user@example.com"}) (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 [out (th/command! data)]
(t/is (th/success? out))
(let [result (:result out)]
(t/is (contains? result :token))))))
(t/deftest register-profile-with-email-as-password
(let [data {::th/type :prepare-register-profile (let [data {::th/type :prepare-register-profile
:email "user@example.com" :email "user@example.com"
:password "USER@example.com"}] :password "USER@example.com"}
out (th/command! data)]
(let [{:keys [result error] :as out} (th/mutation! data)] (t/is (not (th/success? out)))
(t/is (th/ex-info? error)) (let [edata (-> out :error ex-data)]
(t/is (th/ex-of-type? error :validation)) (t/is (= :validation (:type edata)))
(t/is (th/ex-of-code? error :email-as-password))))) (t/is (= :email-as-password (:code edata))))))
(t/deftest test-email-change-request (t/deftest email-change-request
(with-mocks [email-send-mock {:target 'app.emails/send! :return nil}] (with-mocks [mock {:target 'app.emails/send! :return nil}]
(let [profile (th/create-profile* 1) (let [profile (th/create-profile* 1)
pool (:app.db/pool th/*system*) pool (:app.db/pool th/*system*)
data {::th/type :request-email-change data {::th/type :request-email-change
@ -312,7 +414,7 @@
(let [out (th/mutation! data)] (let [out (th/mutation! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:result out))) (t/is (nil? (:result out)))
(let [mock (deref email-send-mock)] (let [mock @mock]
(t/is (= 1 (:call-count mock))) (t/is (= 1 (:call-count mock)))
(t/is (true? (:called? mock))))) (t/is (true? (:called? mock)))))
@ -321,7 +423,7 @@
(let [out (th/mutation! data)] (let [out (th/mutation! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:result out))) (t/is (nil? (:result out)))
(t/is (= 2 (:call-count (deref email-send-mock))))) (t/is (= 2 (:call-count @mock))))
;; with bounces ;; with bounces
(th/create-global-complaint-for pool {:type :bounce :email (:email data)}) (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-info? error))
(t/is (th/ex-of-type? error :validation)) (t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :email-has-permanent-bounces)) (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 (t/deftest email-change-request-without-smtp
(with-mocks [email-send-mock {:target 'app.emails/send! :return nil}] (with-mocks [mock {:target 'app.emails/send! :return nil}]
(with-redefs [app.config/flags #{}] (with-redefs [app.config/flags #{}]
(let [profile (th/create-profile* 1) (let [profile (th/create-profile* 1)
pool (:app.db/pool th/*system*) pool (:app.db/pool th/*system*)
data {::th/type :request-email-change data {::th/type :request-email-change
:profile-id (:id profile) :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) ;; (th/print-result! out)
(t/is (= {:changed true} res)) (t/is (false? (:called? @mock)))
(let [mock (deref email-send-mock)] (let [res (:result out)]
(t/is (false? (:called? mock))))))))) (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}] (with-mocks [mock {:target 'app.emails/send! :return nil}]
(let [profile1 (th/create-profile* 1) (let [profile1 (th/create-profile* 1)
profile2 (th/create-profile* 2 {:is-active true}) profile2 (th/create-profile* 2 {:is-active true})
@ -363,13 +463,13 @@
(let [data (assoc data :email "foo@bar.com") (let [data (assoc data :email "foo@bar.com")
out (th/mutation! data)] out (th/mutation! data)]
(t/is (nil? (:result out))) (t/is (nil? (:result out)))
(t/is (= 0 (:call-count (deref mock))))) (t/is (= 0 (:call-count @mock))))
;; with valid email inactive user ;; with valid email inactive user
(let [data (assoc data :email (:email profile1)) (let [data (assoc data :email (:email profile1))
out (th/mutation! data) out (th/mutation! data)
error (:error out)] 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-info? error))
(t/is (th/ex-of-type? error :validation)) (t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :profile-not-verified))) (t/is (th/ex-of-code? error :profile-not-verified)))
@ -379,7 +479,7 @@
out (th/mutation! data)] out (th/mutation! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (: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 ;; with valid email and active user with global complaints
(th/create-global-complaint-for pool {:type :complaint :email (:email profile2)}) (th/create-global-complaint-for pool {:type :complaint :email (:email profile2)})
@ -387,7 +487,7 @@
out (th/mutation! data)] out (th/mutation! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (: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 ;; with valid email and active user with global bounce
(th/create-global-complaint-for pool {:type :bounce :email (:email profile2)}) (th/create-global-complaint-for pool {:type :bounce :email (:email profile2)})
@ -395,7 +495,7 @@
out (th/mutation! data) out (th/mutation! data)
error (:error out)] error (:error out)]
;; (th/print-result! 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-info? error))
(t/is (th/ex-of-type? error :validation)) (t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :email-has-permanent-bounces))) (t/is (th/ex-of-code? error :email-has-permanent-bounces)))

View file

@ -11,6 +11,7 @@
[app.http :as http] [app.http :as http]
[app.storage :as sto] [app.storage :as sto]
[app.test-helpers :as th] [app.test-helpers :as th]
[app.tokens :as tokens]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.test :as t] [clojure.test :as t]
[datoteka.core :as fs] [datoteka.core :as fs]
@ -19,7 +20,7 @@
(t/use-fixtures :once th/state-init) (t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset) (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}] (with-mocks [mock {:target 'app.emails/send! :return nil}]
(let [profile1 (th/create-profile* 1 {:is-active true}) (let [profile1 (th/create-profile* 1 {:is-active true})
profile2 (th/create-profile* 2 {:is-active true}) profile2 (th/create-profile* 2 {:is-active true})
@ -43,8 +44,7 @@
(:team-id data) "foo@bar.com"])] (:team-id data) "foo@bar.com"])]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (th/success? out))
(t/is (= {} (:result out)))
(t/is (= 1 (:call-count (deref mock)))) (t/is (= 1 (:call-count (deref mock))))
(t/is (= 1 (:num invitation)))) (t/is (= 1 (:num invitation))))
@ -52,7 +52,7 @@
(th/reset-mock! mock) (th/reset-mock! mock)
(let [data (assoc data :email (:email profile2)) (let [data (assoc data :email (:email profile2))
out (th/mutation! data)] out (th/mutation! data)]
(t/is (= {} (:result out))) (t/is (th/success? out))
(t/is (= 1 (:call-count (deref mock))))) (t/is (= 1 (:call-count (deref mock)))))
;; invite user with complaint ;; invite user with complaint
@ -60,35 +60,183 @@
(th/reset-mock! mock) (th/reset-mock! mock)
(let [data (assoc data :email "foo@bar.com") (let [data (assoc data :email "foo@bar.com")
out (th/mutation! data)] out (th/mutation! data)]
(t/is (= {} (:result out))) (t/is (th/success? out))
(t/is (= 1 (:call-count (deref mock))))) (t/is (= 1 (:call-count (deref mock)))))
;; invite user with bounce ;; invite user with bounce
(th/reset-mock! mock) (th/reset-mock! mock)
(th/create-global-complaint-for pool {:type :bounce :email "foo@bar.com"}) (th/create-global-complaint-for pool {:type :bounce :email "foo@bar.com"})
(let [data (assoc data :email "foo@bar.com") (let [data (assoc data :email "foo@bar.com")
out (th/mutation! data) out (th/mutation! data)]
error (:error out)]
(t/is (th/ex-info? error)) (t/is (not (th/success? out)))
(t/is (th/ex-of-type? error :validation)) (t/is (= 0 (:call-count @mock)))
(t/is (th/ex-of-code? error :email-has-permanent-bounces))
(t/is (= 0 (:call-count (deref 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 ;; invite internal user that is muted
(th/reset-mock! mock) (th/reset-mock! mock)
(let [data (assoc data :email (:email profile3))
out (th/mutation! data)
error (:error out)]
(t/is (th/ex-info? error)) (let [data (assoc data :email (:email profile3))
(t/is (th/ex-of-type? error :validation)) out (th/mutation! data)]
(t/is (th/ex-of-code? error :member-is-muted))
(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 (= :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 (t/deftest invite-team-member-with-email-verification-disabled
(with-mocks [mock {:target 'app.emails/send! :return nil}] (with-mocks [mock {:target 'app.emails/send! :return nil}]
(let [profile1 (th/create-profile* 1 {:is-active true}) (let [profile1 (th/create-profile* 1 {:is-active true})
@ -108,20 +256,17 @@
(th/reset-mock! mock) (th/reset-mock! mock)
(let [data (assoc data :email (:email profile2)) (let [data (assoc data :email (:email profile2))
out (th/mutation! data)] out (th/mutation! data)]
(t/is (= {} (:result out))) (t/is (th/success? out))
(t/is (= 0 (:call-count (deref mock))))) (t/is (= 0 (:call-count (deref mock)))))
(let [members (db/query pool :team-profile-rel (let [members (db/query pool :team-profile-rel
{:team-id (:id team) {:team-id (:id team)
:profile-id (:id profile2)})] :profile-id (:id profile2)})]
(t/is (= 1 (count members))) (t/is (= 1 (count members)))
(t/is (true? (-> members first :can-edit)))))))) (t/is (true? (-> members first :can-edit))))))))
(t/deftest team-deletion
(t/deftest test-deletion (let [profile1 (th/create-profile* 1 {:is-active true})
(let [task (:app.tasks.objects-gc/handler th/*system*)
profile1 (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id profile1)}) team (th/create-team* 1 {:profile-id (:id profile1)})
pool (:app.db/pool th/*system*) pool (:app.db/pool th/*system*)
data {::th/type :delete-team data {::th/type :delete-team
@ -130,7 +275,7 @@
;; team is not deleted because it does not meet all ;; team is not deleted because it does not meet all
;; conditions to be deleted. ;; 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)))) (t/is (= 0 (:processed result))))
;; query the list of teams ;; query the list of teams
@ -138,7 +283,7 @@
:profile-id (:id profile1)} :profile-id (:id profile1)}
out (th/query! data)] out (th/query! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (th/success? out))
(let [result (:result out)] (let [result (:result out)]
(t/is (= 2 (count result))) (t/is (= 2 (count result)))
(t/is (= (:id team) (get-in result [1 :id]))) (t/is (= (:id team) (get-in result [1 :id])))
@ -149,21 +294,20 @@
:id (:id team) :id (:id team)
:profile-id (:id profile1)} :profile-id (:id profile1)}
out (th/mutation! params)] out (th/mutation! params)]
;; (th/print-result! out) (t/is (th/success? out)))
(t/is (nil? (:error out))))
;; query the list of teams after soft deletion ;; query the list of teams after soft deletion
(let [data {::th/type :teams (let [data {::th/type :teams
:profile-id (:id profile1)} :profile-id (:id profile1)}
out (th/query! data)] out (th/query! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (th/success? out))
(let [result (:result out)] (let [result (:result out)]
(t/is (= 1 (count result))) (t/is (= 1 (count result)))
(t/is (= (:default-team-id profile1) (get-in result [0 :id]))))) (t/is (= (:default-team-id profile1) (get-in result [0 :id])))))
;; run permanent deletion (should be noop) ;; 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)))) (t/is (= 0 (:processed result))))
;; query the list of projects after hard deletion ;; query the list of projects after hard deletion
@ -172,13 +316,12 @@
:profile-id (:id profile1)} :profile-id (:id profile1)}
out (th/query! data)] out (th/query! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(let [error (:error out) (t/is (not (th/success? out)))
error-data (ex-data error)] (let [edata (-> out :error ex-data)]
(t/is (th/ex-info? error)) (t/is (= :not-found (:type edata)))))
(t/is (= (:type error-data) :not-found))))
;; run permanent deletion ;; 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)))) (t/is (= 1 (:processed result))))
;; query the list of projects of a after hard deletion ;; query the list of projects of a after hard deletion
@ -187,15 +330,12 @@
:profile-id (:id profile1)} :profile-id (:id profile1)}
out (th/query! data)] out (th/query! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(let [error (:error out)
error-data (ex-data error)] (t/is (not (th/success? out)))
(t/is (th/ex-info? error)) (let [edata (-> out :error ex-data)]
(t/is (= (:type error-data) :not-found)))) (t/is (= :not-found (:type edata)))))
)) ))
(t/deftest query-team-invitations (t/deftest query-team-invitations
(let [prof (th/create-profile* 1 {:is-active true}) (let [prof (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id prof)}) team (th/create-team* 1 {:profile-id (:id prof)})
@ -210,7 +350,6 @@
:role "editor" :role "editor"
:valid-until (dt/in-future "48h")}) :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 (db/insert! th/*pool* :team-invitation
{:team-id (:team-id data) {:team-id (:team-id data)
@ -219,7 +358,7 @@
:valid-until (dt/in-past "48h")}) :valid-until (dt/in-past "48h")})
(let [out (th/query! data)] (let [out (th/query! data)]
(t/is (nil? (:error out))) (t/is (th/success? out))
(let [result (:result out) (let [result (:result out)
one (first result) one (first result)
two (second result)] two (second result)]
@ -229,7 +368,6 @@
(t/is (false? (:expired one))) (t/is (false? (:expired one)))
(t/is (true? (:expired two))))))) (t/is (true? (:expired two)))))))
(t/deftest update-team-invitation-role (t/deftest update-team-invitation-role
(let [prof (th/create-profile* 1 {:is-active true}) (let [prof (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id prof)}) team (th/create-team* 1 {:profile-id (:id prof)})
@ -248,13 +386,11 @@
(let [out (th/mutation! data) (let [out (th/mutation! data)
;; retrieve the value from the database and check its content ;; retrieve the value from the database and check its content
result (db/get-by-params th/*pool* :team-invitation res (db/get* th/*pool* :team-invitation
{:team-id (:team-id data) :email-to "test1@mail.com"} {:team-id (:team-id data) :email-to "test1@mail.com"})]
{:check-not-found false})] (t/is (th/success? out))
(t/is (nil? (:error out)))
(t/is (nil? (:result out))) (t/is (nil? (:result out)))
(t/is (= "admin" (:role result)))))) (t/is (= "admin" (:role res))))))
(t/deftest delete-team-invitation (t/deftest delete-team-invitation
(let [prof (th/create-profile* 1 {:is-active true}) (let [prof (th/create-profile* 1 {:is-active true})
@ -273,9 +409,9 @@
(let [out (th/mutation! data) (let [out (th/mutation! data)
;; retrieve the value from the database and check its content ;; retrieve the value from the database and check its content
result (db/get-by-params th/*pool* :team-invitation res (db/get* th/*pool* :team-invitation
{:team-id (:team-id data) :email-to "test1@mail.com"} {:team-id (:team-id data) :email-to "test1@mail.com"})]
{:check-not-found false})]
(t/is (nil? (:error out))) (t/is (th/success? out))
(t/is (nil? (:result out))) (t/is (nil? (:result out)))
(t/is (nil? result))))) (t/is (nil? res)))))

View file

@ -23,9 +23,11 @@
[app.rpc.mutations.projects :as projects] [app.rpc.mutations.projects :as projects]
[app.rpc.mutations.teams :as teams] [app.rpc.mutations.teams :as teams]
[app.util.blob :as blob] [app.util.blob :as blob]
[app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.java.io :as io] [clojure.java.io :as io]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[clojure.test :as t]
[cuerdas.core :as str] [cuerdas.core :as str]
[datoteka.core :as fs] [datoteka.core :as fs]
[environ.core :refer [env]] [environ.core :refer [env]]
@ -277,8 +279,10 @@
(defmacro try-on! (defmacro try-on!
[expr] [expr]
`(try `(try
(let [result# (deref ~expr)
result# (cond-> result# (sv/wrapped? result#) deref)]
{:error nil {:error nil
:result (deref ~expr)} :result result#})
(catch Exception e# (catch Exception e#
{:error (handle-error e#) {:error (handle-error e#)
:result nil}))) :result nil})))
@ -299,6 +303,14 @@
(let [method-fn (get-in *system* [:app.rpc/methods :queries type])] (let [method-fn (get-in *system* [:app.rpc/methods :queries type])]
(try-on! (method-fn (dissoc data ::type))))) (try-on! (method-fn (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 ;; --- UTILS
(defn print-error! (defn print-error!
@ -358,6 +370,10 @@
(let [data (ex-data e)] (let [data (ex-data e)]
(= code (:code data)))) (= code (:code data))))
(defn success?
[{:keys [result error]}]
(nil? error))
(defn tempfile (defn tempfile
[source] [source]
(let [rsc (io/resource source) (let [rsc (io/resource source)
@ -366,29 +382,6 @@
(io/file tmp)) (io/file tmp))
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 (defn pause
[] []
(let [^java.io.Console cnsl (System/console)] (let [^java.io.Console cnsl (System/console)]
@ -408,3 +401,18 @@
[& params] [& params]
(apply db/query *pool* 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 {})))

View file

@ -3,22 +3,22 @@
org.clojure/data.json {:mvn/version "2.4.0"} org.clojure/data.json {:mvn/version "2.4.0"}
org.clojure/tools.cli {:mvn/version "1.0.206"} org.clojure/tools.cli {:mvn/version "1.0.206"}
metosin/jsonista {:mvn/version "0.3.6"} metosin/jsonista {:mvn/version "0.3.6"}
org.clojure/clojurescript {:mvn/version "1.11.57"} org.clojure/clojurescript {:mvn/version "1.11.60"}
;; Logging ;; Logging
org.apache.logging.log4j/log4j-api {: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.17.2"} org.apache.logging.log4j/log4j-core {:mvn/version "2.19.0"}
org.apache.logging.log4j/log4j-web {:mvn/version "2.17.2"} org.apache.logging.log4j/log4j-web {:mvn/version "2.19.0"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.17.2"} org.apache.logging.log4j/log4j-jul {:mvn/version "2.19.0"}
org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.17.2"} org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.18.0"}
org.slf4j/slf4j-api {:mvn/version "2.0.0-alpha1"} 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"} criterium/criterium {:mvn/version "0.4.6"}
expound/expound {:mvn/version "0.9.0"} expound/expound {:mvn/version "0.9.0"}
com.cognitect/transit-clj {:mvn/version "1.0.329"} 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"} java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/promesa {:mvn/version "8.0.450"} funcool/promesa {:mvn/version "8.0.450"}
@ -42,21 +42,22 @@
{:extra-deps {:extra-deps
{org.clojure/tools.namespace {:mvn/version "RELEASE"} {org.clojure/tools.namespace {:mvn/version "RELEASE"}
org.clojure/test.check {: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"} com.bhauman/rebel-readline {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "RELEASE"} criterium/criterium {:mvn/version "RELEASE"}
mockery/mockery {:mvn/version "RELEASE"}} mockery/mockery {:mvn/version "RELEASE"}}
:extra-paths ["test" "dev"]} :extra-paths ["test" "dev"]}
:build :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} :ns-default build}
:test :test
{:extra-paths ["test"] {:extra-paths ["test"]
:extra-deps :extra-deps
{io.github.cognitect-labs/test-runner {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} :exec-fn cognitect.test-runner.api/test}
:shadow-cljs :shadow-cljs

View file

@ -107,12 +107,12 @@ RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \ ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \ case "${ARCH}" in \
aarch64|arm64) \ aarch64|arm64) \
ESUM='37ceaf232a85cce46bcccfd71839854e8b14bf3160e7ef72a676b9cae45ee8af'; \ ESUM='c640fc5e5710dba3f92099a791be50fab54f91cf2c3838cb536ded27ecc562a6'; \
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'; \ BINARY_URL='https://cdn.azul.com/zulu/bin/zulu19.28.81-ca-jdk19.0.0-linux_aarch64.tar.gz'; \
;; \ ;; \
amd64|x86_64) \ amd64|x86_64) \
ESUM='16b1d9d75f22c157af04a1fd9c664324c7f4b5163c022b382a2f2e8897c1b0a2'; \ ESUM='6813da339124261092daab369a1c60dea5f27f4ba9608a16517191d30511a087'; \
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'; \ BINARY_URL='https://cdn.azul.com/zulu/bin/zulu19.28.81-ca-jdk19.0.0-linux_x64.tar.gz'; \
;; \ ;; \
*) \ *) \
echo "Unsupported arch: ${ARCH}"; \ echo "Unsupported arch: ${ARCH}"; \

View file

@ -8,8 +8,7 @@
PENPOT_PUBLIC_URI=http://localhost:9001 PENPOT_PUBLIC_URI=http://localhost:9001
## Feature flags. ## 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 ## Temporal workaround because of bad builtin default

View file

@ -15,7 +15,7 @@
:dev :dev
{:extra-deps {:extra-deps
{thheller/shadow-cljs {:mvn/version "2.19.8"}}} {thheller/shadow-cljs {:mvn/version "2.20.2"}}}
:shadow-cljs :shadow-cljs
{:main-opts ["-m" "shadow.cljs.devtools.cli"]} {:main-opts ["-m" "shadow.cljs.devtools.cli"]}

View file

@ -21,7 +21,7 @@
"xregexp": "^5.0.2" "xregexp": "^5.0.2"
}, },
"devDependencies": { "devDependencies": {
"shadow-cljs": "^2.19.8", "shadow-cljs": "^2.20.2",
"source-map-support": "^0.5.21" "source-map-support": "^0.5.21"
} }
} }

View file

@ -9,7 +9,7 @@
["process" :as proc] ["process" :as proc]
[app.browser :as bwr] [app.browser :as bwr]
[app.common.logging :as l] [app.common.logging :as l]
[app.config] [app.config :as cf]
[app.http :as http] [app.http :as http]
[app.redis :as redis] [app.redis :as redis]
[promesa.core :as p])) [promesa.core :as p]))
@ -19,7 +19,9 @@
(defn start (defn start
[& _] [& _]
(l/info :msg "initializing") (l/info :msg "initializing"
:public-uri (str (cf/get :public-uri))
:version (:full @cf/version))
(p/do! (p/do!
(bwr/init) (bwr/init)
(redis/init) (redis/init)
@ -39,5 +41,6 @@
(http/stop) (http/stop)
(done))) (done)))
(proc/on "uncaughtException" (fn [cause] (proc/on "uncaughtException"
(fn [cause]
(js/console.error cause))) (js/console.error cause)))

View file

@ -77,7 +77,7 @@
:name (:name resource) :name (:name resource)
:status "ended"})))) :status "ended"}))))
on-error (fn [cause] on-error (fn [cause]
(l/error :hint "unexpected error happened on export multiple process" (l/error :hint "unexpected error on export multiple"
:cause cause) :cause cause)
(if wait (if wait
(p/rejected cause) (p/rejected cause)

View file

@ -90,12 +90,13 @@
(fn [{:keys [:response/body :response/status] :as exchange}] (fn [{:keys [:response/body :response/status] :as exchange}]
(cond (cond
(map? body) (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 (-> exchange
(assoc :response/body data) (assoc :response/body data)
(assoc :response/status 200) (assoc :response/status 200)
(update :response/headers assoc "content-type" "application/transit+json") (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) (and (nil? body)
(= 200 status)) (= 200 status))

View file

@ -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" resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b"
integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg== integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg==
shadow-cljs@^2.19.8: shadow-cljs@^2.20.2:
version "2.19.8" version "2.20.2"
resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.19.8.tgz#1ce96cab3e4903bed8d401ffbe88b8939f5454d3" resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.20.2.tgz#24a4b204f1f2288dc4ff2d0a4f3972a6e5307645"
integrity sha512-6qek3mcAP0hrnC5FxrTebBrgLGpOuhlnp06vdxp6g0M5Gl6w2Y0hzSwa1s2K8fMOkzE4/ciQor75b2y64INgaw== integrity sha512-2kzWnV1QM6KBetziCAkCf8BJdnDX2CwiAr4yhvOsiQpaNJcMzwMsJTX/gTHz58yQg0dV5uwPsIyBlvyIfl30rg==
dependencies: dependencies:
node-libs-browser "^2.2.1" node-libs-browser "^2.2.1"
readline-sync "^1.4.7" readline-sync "^1.4.7"

View file

@ -10,9 +10,13 @@
funcool/beicon {:mvn/version "2021.07.05-1"} funcool/beicon {:mvn/version "2021.07.05-1"}
funcool/okulary {:mvn/version "2022.04.11-16"} funcool/okulary {:mvn/version "2022.04.11-16"}
funcool/potok {:mvn/version "2022.04.28-67"} 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/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"} instaparse/instaparse {:mvn/version "1.4.12"}
garden/garden {:git/url "https://github.com/noprompt/garden" garden/garden {:git/url "https://github.com/noprompt/garden"
:git/sha "05590ecb5f6fa670856f3d1ab400aa4961047480"} :git/sha "05590ecb5f6fa670856f3d1ab400aa4961047480"}
@ -32,7 +36,7 @@
:dev :dev
{:extra-paths ["dev"] {:extra-paths ["dev"]
:extra-deps :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"} org.clojure/tools.namespace {:mvn/version "RELEASE"}
cider/cider-nrepl {:mvn/version "0.28.4"}}} cider/cider-nrepl {:mvn/version "0.28.4"}}}

View file

@ -48,7 +48,7 @@
"prettier": "^2.7.1", "prettier": "^2.7.1",
"rimraf": "^3.0.0", "rimraf": "^3.0.0",
"sass": "^1.53.0", "sass": "^1.53.0",
"shadow-cljs": "2.19.9" "shadow-cljs": "2.20.2"
}, },
"dependencies": { "dependencies": {
"@sentry/browser": "^6.17.4", "@sentry/browser": "^6.17.4",

View file

@ -88,7 +88,6 @@
(def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js")) (def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js"))
(def translations (obj/get global "penpotTranslations")) (def translations (obj/get global "penpotTranslations"))
(def themes (obj/get global "penpotThemes")) (def themes (obj/get global "penpotThemes"))
(def sentry-dsn (obj/get global "penpotSentryDsn"))
(def onboarding-form-id (obj/get global "penpotOnboardingQuestionsFormId")) (def onboarding-form-id (obj/get global "penpotOnboardingQuestionsFormId"))
(def build-date (parse-build-date global)) (def build-date (parse-build-date global))

View file

@ -13,7 +13,6 @@
[app.main.data.users :as du] [app.main.data.users :as du]
[app.main.data.websocket :as ws] [app.main.data.websocket :as ws]
[app.main.errors] [app.main.errors]
[app.main.sentry :as sentry]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui :as ui] [app.main.ui :as ui]
[app.main.ui.alert] [app.main.ui.alert]
@ -29,7 +28,7 @@
[debug] [debug]
[features] [features]
[potok.core :as ptk] [potok.core :as ptk]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(log/initialize!) (log/initialize!)
(log/set-level! :root :warn) (log/set-level! :root :warn)
@ -75,7 +74,6 @@
(defn ^:export init (defn ^:export init
[] []
(worker/init!) (worker/init!)
(sentry/init!)
(i18n/init! cf/translations) (i18n/init! cf/translations)
(theme/init! cf/themes) (theme/init! cf/themes)
(init-ui) (init-ui)

View file

@ -12,7 +12,7 @@
[app.main.store :as st] [app.main.store :as st]
[okulary.core :as l] [okulary.core :as l]
[potok.core :as ptk] [potok.core :as ptk]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(log/set-level! :debug) (log/set-level! :debug)

View file

@ -45,7 +45,7 @@
[beicon.core :as rx] [beicon.core :as rx]
[clojure.set :as set] [clojure.set :as set]
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(def ^:const viewbox-decimal-precision 3) (def ^:const viewbox-decimal-precision 3)
(def ^:private default-color clr/canvas) (def ^:private default-color clr/canvas)

View file

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

View file

@ -24,7 +24,7 @@
[app.main.ui.viewer :as viewer] [app.main.ui.viewer :as viewer]
[app.main.ui.workspace :as workspace] [app.main.ui.workspace :as workspace]
[app.util.router :as rt] [app.util.router :as rt]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc on-main-error (mf/defc on-main-error
[{:keys [error] :as props}] [{:keys [error] :as props}]

View file

@ -13,7 +13,7 @@
[app.util.i18n :as i18n :refer [tr t]] [app.util.i18n :as i18n :refer [tr t]]
[app.util.keyboard :as k] [app.util.keyboard :as k]
[goog.events :as events] [goog.events :as events]
[rumext.alpha :as mf]) [rumext.v2 :as mf])
(:import goog.events.EventType)) (:import goog.events.EventType))
(mf/defc alert-dialog (mf/defc alert-dialog

View file

@ -14,7 +14,7 @@
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc terms-login (mf/defc terms-login
[] []

View file

@ -20,7 +20,7 @@
[app.util.router :as rt] [app.util.router :as rt]
[beicon.core :as rx] [beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(def show-alt-login-buttons? (def show-alt-login-buttons?
(some (partial contains? @cf/flags) (some (partial contains? @cf/flags)
@ -83,8 +83,18 @@
form (fm/use-form :spec ::login-form :initial initial) form (fm/use-form :spec ::login-form :initial initial)
on-error on-error
(fn [_] (fn [cause]
(reset! error (tr "errors.wrong-credentials"))) (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 on-success-default
(fn [data] (fn [data]

View file

@ -14,7 +14,7 @@
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt] [app.util.router :as rt]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(s/def ::password-1 ::us/not-empty-string) (s/def ::password-1 ::us/not-empty-string)
(s/def ::password-2 ::us/not-empty-string) (s/def ::password-2 ::us/not-empty-string)

View file

@ -16,7 +16,7 @@
[app.util.router :as rt] [app.util.router :as rt]
[beicon.core :as rx] [beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(s/def ::email ::us/email) (s/def ::email ::us/email)
(s/def ::recovery-request-form (s/keys :req-un [::email])) (s/def ::recovery-request-form (s/keys :req-un [::email]))

View file

@ -20,7 +20,7 @@
[app.util.router :as rt] [app.util.router :as rt]
[beicon.core :as rx] [beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc demo-warning (mf/defc demo-warning
[_] [_]
@ -48,20 +48,23 @@
:opt-un [::invitation-token])) :opt-un [::invitation-token]))
(defn- handle-prepare-register-error (defn- handle-prepare-register-error
[form error] [form {:keys [type code] :as cause}]
(case (:code error) (condp = [type code]
:registration-disabled [:restriction :registration-disabled]
(st/emit! (dm/error (tr "errors.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])] (let [email (get @form [:data :email])]
(st/emit! (dm/error (tr "errors.email-has-permanent-bounces" 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] (swap! form assoc-in [:errors :email]
{:message "errors.email-already-exists"}) {:message "errors.email-already-exists"})
:email-as-password [:validation :email-as-password]
(swap! form assoc-in [:errors :password] (swap! form assoc-in [:errors :password]
{:message "errors.email-as-password"}) {:message "errors.email-as-password"})

View file

@ -17,7 +17,7 @@
[app.util.router :as rt] [app.util.router :as rt]
[app.util.timers :as ts] [app.util.timers :as ts]
[beicon.core :as rx] [beicon.core :as rx]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(defmulti handle-token (fn [token] (:iss token))) (defmulti handle-token (fn [token] (:iss token)))
@ -62,10 +62,10 @@
[{:keys [route] :as props}] [{:keys [route] :as props}]
(let [token (get-in route [:query-params :token]) (let [token (get-in route [:query-params :token])
bad-token (mf/use-state false)] bad-token (mf/use-state false)]
(mf/use-effect
(fn [] (mf/with-effect []
(dom/set-html-title (tr "title.default")) (dom/set-html-title (tr "title.default"))
(->> (rp/mutation :verify-token {:token token}) (->> (rp/command! :verify-token {:token token})
(rx/subs (rx/subs
(fn [tdata] (fn [tdata]
(handle-token tdata)) (handle-token tdata))
@ -75,6 +75,7 @@
(= :invalid-token code) (= :invalid-token code)
(= :token-expired (:reason error))) (= :token-expired (:reason error)))
(reset! bad-token true) (reset! bad-token true)
(= :email-already-exists code) (= :email-already-exists code)
(let [msg (tr "errors.email-already-exists")] (let [msg (tr "errors.email-already-exists")]
(ts/schedule 100 #(st/emit! (dm/error msg))) (ts/schedule 100 #(st/emit! (dm/error msg)))
@ -88,7 +89,7 @@
:else :else
(let [msg (tr "errors.generic")] (let [msg (tr "errors.generic")]
(ts/schedule 100 #(st/emit! (dm/error msg))) (ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :auth-login))))))))) (st/emit! (rt/nav :auth-login))))))))
(if @bad-token (if @bad-token
[:> static/static-header {} [:> static/static-header {}

View file

@ -22,7 +22,7 @@
[app.util.time :as dt] [app.util.time :as dt]
[cuerdas.core :as str] [cuerdas.core :as str]
[okulary.core :as l] [okulary.core :as l]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc resizing-textarea (mf/defc resizing-textarea
{::mf/wrap-props false} {::mf/wrap-props false}

View file

@ -7,7 +7,7 @@
(ns app.main.ui.components.code-block (ns app.main.ui.components.code-block
(:require (:require
["highlight.js" :as hljs] ["highlight.js" :as hljs]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc code-block [{:keys [code type]}] (mf/defc code-block [{:keys [code type]}]
(let [block-ref (mf/use-ref)] (let [block-ref (mf/use-ref)]

View file

@ -9,7 +9,7 @@
[app.util.color :as uc] [app.util.color :as uc]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(defn gradient-type->string [type] (defn gradient-type->string [type]
(case type (case type

View file

@ -13,7 +13,7 @@
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.object :as obj] [app.util.object :as obj]
[goog.events :as events] [goog.events :as events]
[rumext.alpha :as mf]) [rumext.v2 :as mf])
(:import goog.events.EventType)) (:import goog.events.EventType))
(defn clean-color (defn clean-color

View file

@ -15,7 +15,7 @@
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.object :as obj] [app.util.object :as obj]
[goog.object :as gobj] [goog.object :as gobj]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc context-menu (mf/defc context-menu
{::mf/wrap-props false} {::mf/wrap-props false}

View file

@ -10,7 +10,7 @@
[app.util.timers :as timers] [app.util.timers :as timers]
[app.util.webapi :as wapi] [app.util.webapi :as wapi]
[beicon.core :as rx] [beicon.core :as rx]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc copy-button [{:keys [data on-copied]}] (mf/defc copy-button [{:keys [data on-copied]}]
(let [just-copied (mf/use-state false)] (let [just-copied (mf/use-state false)]

View file

@ -12,7 +12,7 @@
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[goog.events :as events] [goog.events :as events]
[goog.object :as gobj] [goog.object :as gobj]
[rumext.alpha :as mf]) [rumext.v2 :as mf])
(:import goog.events.EventType)) (:import goog.events.EventType))
(mf/defc dropdown' (mf/defc dropdown'

View file

@ -10,7 +10,7 @@
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.timers :as timers] [app.util.timers :as timers]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc editable-label (mf/defc editable-label
[{:keys [value on-change on-cancel editing? disable-dbl-click? class-name] :as props}] [{:keys [value on-change on-cancel editing? disable-dbl-click? class-name] :as props}]

View file

@ -15,7 +15,7 @@
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.timers :as timers] [app.util.timers :as timers]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc editable-select (mf/defc editable-select
[{:keys [value type options class on-change placeholder on-blur] :as params}] [{:keys [value type options class on-change placeholder on-blur] :as params}]

View file

@ -8,7 +8,7 @@
(:require (:require
[app.main.store :as st] [app.main.store :as st]
[app.util.dom :as dom] [app.util.dom :as dom]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc file-uploader (mf/defc file-uploader
{::mf/forward-ref true} {::mf/forward-ref true}

View file

@ -17,7 +17,7 @@
[cljs.core :as c] [cljs.core :as c]
[clojure.string] [clojure.string]
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(def form-ctx (mf/create-context nil)) (def form-ctx (mf/create-context nil))
(def use-form fm/use-form) (def use-form fm/use-form)

View file

@ -16,7 +16,7 @@
[app.util.simple-math :as sm] [app.util.simple-math :as sm]
[cuerdas.core :as str] [cuerdas.core :as str]
[goog.events :as events] [goog.events :as events]
[rumext.alpha :as mf]) [rumext.v2 :as mf])
(:import goog.events.EventType)) (:import goog.events.EventType))
(mf/defc numeric-input (mf/defc numeric-input

View file

@ -10,7 +10,7 @@
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc select [{:keys [default-value options class on-change]}] (mf/defc select [{:keys [default-value options class on-change]}]
(let [state (mf/use-state {:id (uuid/next) (let [state (mf/use-state {:id (uuid/next)

View file

@ -7,7 +7,7 @@
(ns app.main.ui.components.shape-icon (ns app.main.ui.components.shape-icon
(:require (:require
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc element-icon (mf/defc element-icon

View file

@ -8,7 +8,7 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc tab-element (mf/defc tab-element
{::mf/wrap-props false} {::mf/wrap-props false}

View file

@ -13,7 +13,7 @@
[app.util.i18n :as i18n :refer [tr t]] [app.util.i18n :as i18n :refer [tr t]]
[app.util.keyboard :as k] [app.util.keyboard :as k]
[goog.events :as events] [goog.events :as events]
[rumext.alpha :as mf]) [rumext.v2 :as mf])
(:import goog.events.EventType)) (:import goog.events.EventType))
(mf/defc confirm-dialog (mf/defc confirm-dialog

View file

@ -6,7 +6,7 @@
(ns app.main.ui.context (ns app.main.ui.context
(:require (:require
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(def render-id (mf/create-context nil)) (def render-id (mf/create-context nil))

View file

@ -9,7 +9,7 @@
(:require (:require
[app.util.timers :as ts] [app.util.timers :as ts]
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
;; Static cursors ;; Static cursors
(def comments (cursor-ref :comments 0 2 20)) (def comments (cursor-ref :comments 0 2 20))

View file

@ -37,7 +37,7 @@
[goog.events :as events] [goog.events :as events]
[okulary.core :as l] [okulary.core :as l]
[potok.core :as ptk] [potok.core :as ptk]
[rumext.alpha :as mf]) [rumext.v2 :as mf])
(:import goog.events.EventType)) (:import goog.events.EventType))
(defn ^boolean uuid-str? (defn ^boolean uuid-str?

View file

@ -14,7 +14,7 @@
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(s/def ::member-id ::us/uuid) (s/def ::member-id ::us/uuid)
(s/def ::leave-modal-form (s/def ::leave-modal-form

View file

@ -17,7 +17,7 @@
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[potok.core :as ptk] [potok.core :as ptk]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc comments-section (mf/defc comments-section
[{:keys [profile team]}] [{:keys [profile team]}]

View file

@ -15,7 +15,7 @@
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[beicon.core :as rx] [beicon.core :as rx]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(def ^:const options [:all :merge :detach]) (def ^:const options [:all :merge :detach])

View file

@ -19,7 +19,7 @@
[app.util.router :as rt] [app.util.router :as rt]
[beicon.core :as rx] [beicon.core :as rx]
[potok.core :as ptk] [potok.core :as ptk]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(defn get-project-name (defn get-project-name
[project] [project]

View file

@ -20,7 +20,7 @@
[app.util.webapi :as wapi] [app.util.webapi :as wapi]
[beicon.core :as rx] [beicon.core :as rx]
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc header (mf/defc header
[{:keys [project on-create-clicked] :as props}] [{:keys [project on-create-clicked] :as props}]

View file

@ -20,7 +20,7 @@
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[beicon.core :as rx] [beicon.core :as rx]
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(defn- use-set-page-title (defn- use-set-page-title
[team section] [team section]

View file

@ -6,9 +6,10 @@
(ns app.main.ui.dashboard.grid (ns app.main.ui.dashboard.grid
(:require (:require
[app.common.data.macros :as dm]
[app.common.logging :as log] [app.common.logging :as log]
[app.main.data.dashboard :as dd] [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.features :as features]
[app.main.fonts :as fonts] [app.main.fonts :as fonts]
[app.main.refs :as refs] [app.main.refs :as refs]
@ -19,42 +20,56 @@
[app.main.ui.dashboard.import :refer [use-import-file]] [app.main.ui.dashboard.import :refer [use-import-file]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.dashboard.placeholder :refer [empty-placeholder loading-placeholder]] [app.main.ui.dashboard.placeholder :refer [empty-placeholder loading-placeholder]]
[app.main.ui.hooks :as h]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.worker :as wrk] [app.main.worker :as wrk]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.dom.dnd :as dnd] [app.util.dom.dnd :as dnd]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.perf :as perf]
[app.util.time :as dt] [app.util.time :as dt]
[app.util.timers :as ts] [app.util.timers :as ts]
[beicon.core :as rx] [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 ;; --- Grid Item Thumbnail
(defn ask-for-thumbnail (defn ask-for-thumbnail
"Creates some hooks to handle the files thumbnails cache" "Creates some hooks to handle the files thumbnails cache"
[file] [file]
(let [components-v2 (features/active-feature? :components-v2)]
(wrk/ask! {:cmd :thumbnails/generate (wrk/ask! {:cmd :thumbnails/generate
:revn (:revn file) :revn (:revn file)
:file-id (:id file) :file-id (:id file)
:components-v2 components-v2}))) :file-name (:name file)
:components-v2 (features/active-feature? :components-v2)}))
(mf/defc grid-item-thumbnail (mf/defc grid-item-thumbnail
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
[{:keys [file] :as props}] [{:keys [file] :as props}]
(let [container (mf/use-ref)] (let [container (mf/use-ref)
(mf/with-effect [file] bgcolor (dm/get-in file [:data :options :background])
visible? (h/use-visible container :once? true)]
(mf/with-effect [file visible?]
(when visible?
(let [tp (perf/tpoint)]
(->> (ask-for-thumbnail file) (->> (ask-for-thumbnail file)
(rx/subscribe-on :af)
(rx/subs (fn [{:keys [data fonts] :as params}] (rx/subs (fn [{:keys [data fonts] :as params}]
(run! fonts/ensure-loaded! fonts) (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)] (when-let [node (mf/ref-val container)]
(dom/set-html! node data)))))) (dom/set-html! node data))))))))
[:div.grid-item-th {:style {:background-color (get-in file [:data :options :background])} [:div.grid-item-th
{:style {:background-color bgcolor}
:ref container} :ref container}
i/loader-pencil])) i/loader-pencil]))
@ -144,6 +159,7 @@
(mf/defc grid-item-metadata (mf/defc grid-item-metadata
[{:keys [modified-at]}] [{:keys [modified-at]}]
(let [locale (mf/deref i18n/locale) (let [locale (mf/deref i18n/locale)
time (dt/timeago modified-at {:locale locale})] time (dt/timeago modified-at {:locale locale})]
[:span.date [:span.date
@ -165,12 +181,13 @@
:edition false}) :edition false})
selected-files (mf/deref refs/dashboard-selected-files) selected-files (mf/deref refs/dashboard-selected-files)
dashboard-local (mf/deref refs/dashboard-local) dashboard-local (mf/deref refs/dashboard-local)
item-ref (mf/use-ref) node-ref (mf/use-ref)
menu-ref (mf/use-ref) menu-ref (mf/use-ref)
selected? (contains? selected-files file-id) selected? (contains? selected-files file-id)
on-menu-close on-menu-close
(mf/use-callback (mf/use-fn
#(swap! local assoc :menu-open false)) #(swap! local assoc :menu-open false))
on-select on-select
@ -184,7 +201,7 @@
(st/emit! (dd/toggle-file-select file))))) (st/emit! (dd/toggle-file-select file)))))
on-navigate on-navigate
(mf/use-callback (mf/use-fn
(mf/deps file) (mf/deps file)
(fn [event] (fn [event]
(let [menu-icon (mf/ref-val menu-ref) (let [menu-icon (mf/ref-val menu-ref)
@ -193,14 +210,14 @@
(st/emit! (dd/go-to-workspace file)))))) (st/emit! (dd/go-to-workspace file))))))
on-drag-start on-drag-start
(mf/use-callback (mf/use-fn
(mf/deps selected-files) (mf/deps selected-files)
(fn [event] (fn [event]
(let [offset (dom/get-offset-position (.-nativeEvent event)) (let [offset (dom/get-offset-position (.-nativeEvent event))
select-current? (not (contains? selected-files (:id file))) 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 counter-el (create-counter-element item-el
(if select-current? (if select-current?
1 1
@ -221,7 +238,7 @@
(ts/raf #(.removeChild ^js item-el counter-el))))) (ts/raf #(.removeChild ^js item-el counter-el)))))
on-menu-click on-menu-click
(mf/use-callback (mf/use-fn
(mf/deps file selected?) (mf/deps file selected?)
(fn [event] (fn [event]
(dom/prevent-default event) (dom/prevent-default event)
@ -236,14 +253,14 @@
:menu-pos position)))) :menu-pos position))))
edit edit
(mf/use-callback (mf/use-fn
(mf/deps file) (mf/deps file)
(fn [name] (fn [name]
(st/emit! (dd/rename-file (assoc file :name name))) (st/emit! (dd/rename-file (assoc file :name name)))
(swap! local assoc :edition false))) (swap! local assoc :edition false)))
on-edit on-edit
(mf/use-callback (mf/use-fn
(mf/deps file) (mf/deps file)
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
@ -251,16 +268,14 @@
:edition true :edition true
:menu-open false)))] :menu-open false)))]
(mf/use-effect (mf/with-effect [selected? local]
(mf/deps selected? local)
(fn []
(when (and (not selected?) (:menu-open @local)) (when (and (not selected?) (:menu-open @local))
(swap! local assoc :menu-open false)))) (swap! local assoc :menu-open false)))
[:div.grid-item.project-th [:div.grid-item.project-th
{:class (dom/classnames :selected selected? {:class (dom/classnames :selected selected?
:library library-view?) :library library-view?)
:ref item-ref :ref node-ref
:draggable true :draggable true
:on-click on-select :on-click on-select
:on-double-click on-navigate :on-double-click on-navigate
@ -296,13 +311,15 @@
:origin origin :origin origin
:dashboard-local dashboard-local}])]]])) :dashboard-local dashboard-local}])]]]))
(mf/defc grid (mf/defc grid
[{:keys [files project on-create-clicked origin limit library-view?] :as props}] [{:keys [files project on-create-clicked origin limit library-view?] :as props}]
(let [dragging? (mf/use-state false) (let [dragging? (mf/use-state false)
project-id (:id project) project-id (:id project)
node-ref (mf/use-var nil)
on-finish-import on-finish-import
(mf/use-callback (mf/use-fn
(fn [] (fn []
(st/emit! (dd/fetch-files {:project-id project-id}) (st/emit! (dd/fetch-files {:project-id project-id})
(dd/fetch-shared-files) (dd/fetch-shared-files)
@ -311,7 +328,7 @@
import-files (use-import-file project-id on-finish-import) import-files (use-import-file project-id on-finish-import)
on-drag-enter on-drag-enter
(mf/use-callback (mf/use-fn
(fn [e] (fn [e]
(when (or (dnd/has-type? e "Files") (when (or (dnd/has-type? e "Files")
(dnd/has-type? e "application/x-moz-file")) (dnd/has-type? e "application/x-moz-file"))
@ -319,32 +336,34 @@
(reset! dragging? true)))) (reset! dragging? true))))
on-drag-over on-drag-over
(mf/use-callback (mf/use-fn
(fn [e] (fn [e]
(when (or (dnd/has-type? e "Files") (when (or (dnd/has-type? e "Files")
(dnd/has-type? e "application/x-moz-file")) (dnd/has-type? e "application/x-moz-file"))
(dom/prevent-default e)))) (dom/prevent-default e))))
on-drag-leave on-drag-leave
(mf/use-callback (mf/use-fn
(fn [e] (fn [e]
(when-not (dnd/from-child? e) (when-not (dnd/from-child? e)
(reset! dragging? false)))) (reset! dragging? false))))
on-drop on-drop
(mf/use-callback (mf/use-fn
(fn [e] (fn [e]
(when (or (dnd/has-type? e "Files") (when (or (dnd/has-type? e "Files")
(dnd/has-type? e "application/x-moz-file")) (dnd/has-type? e "application/x-moz-file"))
(dom/prevent-default e) (dom/prevent-default e)
(reset! dragging? false) (reset! dragging? false)
(import-files (.-files (.-dataTransfer e))))))] (import-files (.-files (.-dataTransfer e))))))
]
[:section.dashboard-grid {:on-drag-enter on-drag-enter [:section.dashboard-grid
{:on-drag-enter on-drag-enter
:on-drag-over on-drag-over :on-drag-over on-drag-over
:on-drag-leave on-drag-leave :on-drag-leave on-drag-leave
:on-drop on-drop} :on-drop on-drop
:ref node-ref}
(cond (cond
(nil? files) (nil? files)
[:& loading-placeholder] [:& loading-placeholder]
@ -352,8 +371,10 @@
(seq files) (seq files)
[:div.grid-row [:div.grid-row
{:style {:grid-template-columns (str "repeat(" limit ", 1fr)")}} {:style {:grid-template-columns (str "repeat(" limit ", 1fr)")}}
(when @dragging? (when @dragging?
[:div.grid-item]) [:div.grid-item])
(for [item files] (for [item files]
[:& grid-item [:& grid-item
{:file item {:file item
@ -361,8 +382,10 @@
:navigate? true :navigate? true
:origin origin :origin origin
:library-view? library-view?}])] :library-view? library-view?}])]
:else :else
[:& empty-placeholder {:default? (:is-default project) [:& empty-placeholder
{:default? (:is-default project)
:on-create-clicked on-create-clicked :on-create-clicked on-create-clicked
:project project :project project
:limit limit :limit limit
@ -370,12 +393,10 @@
(mf/defc line-grid-row (mf/defc line-grid-row
[{:keys [files selected-files dragging? limit] :as props}] [{:keys [files selected-files dragging? limit] :as props}]
(let [limit (if dragging? (let [limit (if dragging? (dec limit) limit)]
(dec limit)
limit)]
[:div.grid-row.no-wrap [:div.grid-row.no-wrap
{:style {:grid-template-columns (str "repeat(" limit ", 1fr)")}} {:style {:grid-template-columns (dm/str "repeat(" limit ", 1fr)")}}
(when dragging? (when dragging?
[:div.grid-item]) [:div.grid-item])
(for [item (take limit files)] (for [item (take limit files)]
@ -396,8 +417,8 @@
selected-project (mf/deref refs/dashboard-selected-project) selected-project (mf/deref refs/dashboard-selected-project)
on-finish-import on-finish-import
(mf/use-callback (mf/use-fn
(mf/deps (:id team)) (mf/deps team-id)
(fn [] (fn []
(st/emit! (dd/fetch-recent-files (:id team)) (st/emit! (dd/fetch-recent-files (:id team))
(dd/clear-selected-files)))) (dd/clear-selected-files))))
@ -405,7 +426,7 @@
import-files (use-import-file project-id on-finish-import) import-files (use-import-file project-id on-finish-import)
on-drag-enter on-drag-enter
(mf/use-callback (mf/use-fn
(mf/deps selected-project) (mf/deps selected-project)
(fn [e] (fn [e]
(when (dnd/has-type? e "penpot/files") (when (dnd/has-type? e "penpot/files")
@ -421,7 +442,7 @@
(reset! dragging? true)))) (reset! dragging? true))))
on-drag-over on-drag-over
(mf/use-callback (mf/use-fn
(fn [e] (fn [e]
(when (or (dnd/has-type? e "penpot/files") (when (or (dnd/has-type? e "penpot/files")
(dnd/has-type? e "Files") (dnd/has-type? e "Files")
@ -429,19 +450,19 @@
(dom/prevent-default e)))) (dom/prevent-default e))))
on-drag-leave on-drag-leave
(mf/use-callback (mf/use-fn
(fn [e] (fn [e]
(when-not (dnd/from-child? e) (when-not (dnd/from-child? e)
(reset! dragging? false)))) (reset! dragging? false))))
on-drop-success on-drop-success
(fn [] (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/fetch-recent-files (:id team))
(dd/clear-selected-files))) (dd/clear-selected-files)))
on-drop on-drop
(mf/use-callback (mf/use-fn
(mf/deps files selected-files) (mf/deps files selected-files)
(fn [e] (fn [e]
(when (or (dnd/has-type? e "Files") (when (or (dnd/has-type? e "Files")

View file

@ -24,7 +24,7 @@
[app.util.webapi :as wapi] [app.util.webapi :as wapi]
[beicon.core :as rx] [beicon.core :as rx]
[potok.core :as ptk] [potok.core :as ptk]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(log/set-level! :debug) (log/set-level! :debug)

View file

@ -9,7 +9,7 @@
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc inline-edition (mf/defc inline-edition
[{:keys [content on-end] :as props}] [{:keys [content on-end] :as props}]

View file

@ -16,7 +16,7 @@
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.webapi :as wapi] [app.util.webapi :as wapi]
[beicon.core :as rx] [beicon.core :as rx]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc libraries-page (mf/defc libraries-page
[{:keys [team] :as props}] [{:keys [team] :as props}]

View file

@ -8,7 +8,7 @@
(:require (:require
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc empty-placeholder (mf/defc empty-placeholder
[{:keys [dragging? on-create-clicked project limit origin] :as props}] [{:keys [dragging? on-create-clicked project limit origin] :as props}]

View file

@ -19,7 +19,7 @@
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt] [app.util.router :as rt]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(s/def ::project some?) (s/def ::project some?)
(s/def ::show? boolean?) (s/def ::show? boolean?)

View file

@ -6,10 +6,11 @@
(ns app.main.ui.dashboard.projects (ns app.main.ui.dashboard.projects
(:require (:require
[app.common.data :as d]
[app.common.math :as mth] [app.common.math :as mth]
[app.main.data.dashboard :as dd] [app.main.data.dashboard :as dd]
[app.main.data.events :as ev] [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.modal :as modal]
[app.main.data.users :as du] [app.main.data.users :as du]
[app.main.refs :as refs] [app.main.refs :as refs]
@ -27,7 +28,7 @@
[cuerdas.core :as str] [cuerdas.core :as str]
[okulary.core :as l] [okulary.core :as l]
[potok.core :as ptk] [potok.core :as ptk]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc header (mf/defc header
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
@ -43,8 +44,15 @@
(mf/defc team-hero (mf/defc team-hero
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
[{:keys [team close-banner] :as props}] [{:keys [team close-banner] :as props}]
(let [go-members #(st/emit! (dd/go-to-team-members)) (let [go-members (mf/use-fn #(st/emit! (dd/go-to-team-members)))
invite-member #(st/emit! (modal/show {:type :invite-members :team team :origin :hero}))]
invite-member
(mf/use-fn
(mf/deps team)
(fn []
(st/emit! (modal/show {:type :invite-members
:team team
:origin :hero}))))]
[:div.team-hero [:div.team-hero
[:img {:src "images/deco-team-banner.png" :border "0"}] [:img {:src "images/deco-team-banner.png" :border "0"}]
[:div.text [:div.text
@ -52,7 +60,9 @@
[:div.info [:div.info
[:span (tr "dasboard.team-hero.text")] [:span (tr "dasboard.team-hero.text")]
[:a {:on-click go-members} (tr "dasboard.team-hero.management")]]] [: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} [:button.close {:on-click close-banner}
[:span i/close]]])) [:span i/close]]]))
@ -61,34 +71,36 @@
(mf/defc tutorial-project (mf/defc tutorial-project
[{:keys [close-tutorial default-project-id] :as props}] [{:keys [close-tutorial default-project-id] :as props}]
(let [state (mf/use-state (let [state (mf/use-state {:status :waiting
{:status :waiting
:file nil}) :file nil})
template (->> (mf/deref builtin-templates) templates (mf/deref builtin-templates)
(filter #(= (:id %) "tutorial-for-beginners")) template (d/seek #(= (:id %) "tutorial-for-beginners") templates)
first)
on-template-cloned-success on-template-cloned-success
(mf/use-callback (mf/use-fn
(mf/deps default-project-id)
(fn [response] (fn [response]
(swap! state #(assoc % :status :success :file (:first 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"}) (st/emit! (dd/go-to-workspace {:id (first response) :project-id default-project-id :name "tutorial"})
(du/update-profile-props {:viewed-tutorial? true})))) (du/update-profile-props {:viewed-tutorial? true}))))
on-template-cloned-error on-template-cloned-error
(mf/use-fn
(fn [] (fn []
(swap! state #(assoc % :status :waiting)) (swap! state #(assoc % :status :waiting))
(st/emit! (st/emit!
(dm/error (tr "dashboard.libraries-and-templates.import-error")))) (msg/error (tr "dashboard.libraries-and-templates.import-error")))))
download-tutorial download-tutorial
(mf/use-fn
(mf/deps template default-project-id)
(fn [] (fn []
(let [mdata {:on-success on-template-cloned-success :on-error on-template-cloned-error} (let [mdata {:on-success on-template-cloned-success :on-error on-template-cloned-error}
params {:project-id default-project-id :template-id (:id template)}] params {:project-id default-project-id :template-id (:id template)}]
(swap! state #(assoc % :status :importing)) (swap! state #(assoc % :status :importing))
(st/emit! (with-meta (dd/clone-template (with-meta params mdata)) (st/emit! (with-meta (dd/clone-template (with-meta params mdata))
{::ev/origin "get-started-hero-block"}))))] {::ev/origin "get-started-hero-block"})))))]
[:div.tutorial [:div.tutorial
[:div.img] [:div.img]
[:div.text [:div.text
@ -98,9 +110,8 @@
(case (:status @state) (case (:status @state)
:waiting (tr "dasboard.tutorial-hero.start") :waiting (tr "dasboard.tutorial-hero.start")
:importing [:span.loader i/loader-pencil] :importing [:span.loader i/loader-pencil]
:success "" :success "")]]
)
]]
[:button.close [:button.close
{:on-click close-tutorial} {:on-click close-tutorial}
[:span.icon i/close]]])) [:span.icon i/close]]]))
@ -128,21 +139,15 @@
[{:keys [project first? team files] :as props}] [{:keys [project first? team files] :as props}]
(let [locale (mf/deref i18n/locale) (let [locale (mf/deref i18n/locale)
file-count (or (:count project) 0) file-count (or (:count project) 0)
project-id (:id project)
dstate (mf/deref refs/dashboard-local) dstate (mf/deref refs/dashboard-local)
edit-id (:project-for-edit dstate) edit-id (:project-for-edit dstate)
local local (mf/use-state {:menu-open false
(mf/use-state {:menu-open false
:menu-pos nil :menu-pos nil
:edition? (= (:id project) edit-id)}) :edition? (= (:id project) edit-id)})
on-nav
(mf/use-callback
(mf/deps project)
#(st/emit! (rt/nav :dashboard-files {:team-id (:team-id project)
:project-id (:id project)})))
width (mf/use-state nil) width (mf/use-state nil)
rowref (mf/use-ref) rowref (mf/use-ref)
itemsize (if (>= @width 1030) itemsize (if (>= @width 1030)
@ -154,6 +159,12 @@
limit (min 10 nitems) limit (min 10 nitems)
limit (max 1 limit) limit (max 1 limit)
on-nav
(mf/use-fn
(mf/deps project)
(fn []
(st/emit! (rt/nav :dashboard-files {:team-id (:team-id project)
:project-id project-id}))))
toggle-pin toggle-pin
(mf/use-callback (mf/use-callback
(mf/deps project) (mf/deps project)
@ -209,8 +220,7 @@
(dd/fetch-recent-files (:id team)) (dd/fetch-recent-files (:id team))
(dd/clear-selected-files))))] (dd/clear-selected-files))))]
(mf/use-effect (mf/with-effect
(fn []
(let [node (mf/ref-val rowref) (let [node (mf/ref-val rowref)
mnt? (volatile! true) mnt? (volatile! true)
sub (->> (wapi/observe-resize node) sub (->> (wapi/observe-resize node)
@ -223,8 +233,11 @@
(reset! width row-width))))))] (reset! width row-width))))))]
(fn [] (fn []
(vreset! mnt? false) (vreset! mnt? false)
(rx/dispose! sub))))) (rx/dispose! sub))))
[:div.dashboard-project-row {:class (when first? "first")}
[:div.dashboard-project-row
{:class (when first? "first")}
[:div.project {:ref rowref} [:div.project {:ref rowref}
[:div.project-name-wrapper [:div.project-name-wrapper
(if (:edition? @local) (if (:edition? @local)
@ -265,6 +278,7 @@
[:a.btn-secondary.btn-small.tooltip.tooltip-bottom [:a.btn-secondary.btn-small.tooltip.tooltip-bottom
{:on-click on-menu-click :alt (tr "dashboard.options") :data-test "project-options"} {:on-click on-menu-click :alt (tr "dashboard.options") :data-test "project-options"}
i/actions]]] i/actions]]]
(when (and (> limit 0) (when (and (> limit 0)
(> file-count limit)) (> file-count limit))
[:div.show-more {:on-click on-nav} [:div.show-more {:on-click on-nav}
@ -290,53 +304,53 @@
(reverse)) (reverse))
recent-map (mf/deref recent-files-ref) recent-map (mf/deref recent-files-ref)
props (some-> profile (get :props {})) 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) tutorial-viewed? (:viewed-tutorial? props true)
walkthrough-viewed? (:viewed-walkthrough? props true) walkthrough-viewed? (:viewed-walkthrough? props true)
close-banner (fn [] team-id (:id team)
(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 (fn [] close-banner
(st/emit! (mf/use-fn
(du/update-profile-props {:viewed-tutorial? true}) (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" (ptk/event ::ev/event {::ev/name "dont-show"
::ev/origin "get-started-hero-block" ::ev/origin "get-started-hero-block"
:type "tutorial" :type "tutorial"
:section "dashboard"}))) :section "dashboard"}))))
close-walkthrough
close-walkthrough (fn [] (mf/use-fn
(st/emit! (fn []
(du/update-profile-props {:viewed-walkthrough? true}) (st/emit! (du/update-profile-props {:viewed-walkthrough? true})
(ptk/event ::ev/event {::ev/name "dont-show" (ptk/event ::ev/event {::ev/name "dont-show"
::ev/origin "get-started-hero-block" ::ev/origin "get-started-hero-block"
:type "walkthrough" :type "walkthrough"
:section "dashboard"})))] :section "dashboard"}))))]
(mf/use-effect (mf/with-effect [team]
(mf/deps team)
(fn []
(let [tname (if (:is-default team) (let [tname (if (:is-default team)
(tr "dashboard.your-penpot") (tr "dashboard.your-penpot")
(:name team))] (:name team))]
(dom/set-html-title (tr "title.dashboard.projects" tname))))) (dom/set-html-title (tr "title.dashboard.projects" tname))))
(mf/use-effect (mf/with-effect [team-id]
(mf/deps (:id team)) (st/emit! (dd/fetch-recent-files team-id)
(fn [] (dd/clear-selected-files)))
(st/emit! (dd/fetch-recent-files (:id team))
(dd/clear-selected-files))))
(when (seq projects) (when (seq projects)
[:* [:*
[:& header] [:& header]
(when (and team-hero? (not (:is-default team)))
[:& team-hero (when team-hero?
{:team team [:& team-hero {:team team :close-banner close-banner}])
:close-banner close-banner}])
(when (or (not tutorial-viewed?) (not walkthrough-viewed?)) (when (or (not tutorial-viewed?) (not walkthrough-viewed?))
[:div.hero-projects [:div.hero-projects
(when (and (not tutorial-viewed?) (:is-default team)) (when (and (not tutorial-viewed?) (:is-default team))
@ -358,5 +372,5 @@
:team team :team team
:files files :files files
:first? (= project (first projects)) :first? (= project (first projects))
:key (:id project)}]))]]))) :key id}]))]])))

View file

@ -16,7 +16,7 @@
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.webapi :as wapi] [app.util.webapi :as wapi]
[beicon.core :as rx] [beicon.core :as rx]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc search-page (mf/defc search-page
[{:keys [team search-term] :as props}] [{:keys [team search-term] :as props}]

View file

@ -33,7 +33,7 @@
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[goog.functions :as f] [goog.functions :as f]
[potok.core :as ptk] [potok.core :as ptk]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc sidebar-project (mf/defc sidebar-project
[{:keys [item selected?] :as props}] [{:keys [item selected?] :as props}]

View file

@ -27,7 +27,7 @@
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[beicon.core :as rx] [beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc header (mf/defc header
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}

View file

@ -17,7 +17,7 @@
[app.util.router :as rt] [app.util.router :as rt]
[beicon.core :as rx] [beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(s/def ::name ::us/not-empty-string) (s/def ::name ::us/not-empty-string)
(s/def ::team-form (s/def ::team-form

View file

@ -15,7 +15,7 @@
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as k] [app.util.keyboard :as k]
[goog.events :as events] [goog.events :as events]
[rumext.alpha :as mf]) [rumext.v2 :as mf])
(:import goog.events.EventType)) (:import goog.events.EventType))
(mf/defc delete-shared-dialog (mf/defc delete-shared-dialog

View file

@ -20,7 +20,7 @@
[app.util.i18n :as i18n :refer [tr c]] [app.util.i18n :as i18n :refer [tr c]]
[app.util.strings :as ust] [app.util.strings :as ust]
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc export-multiple-dialog (mf/defc export-multiple-dialog
[{:keys [exports title cmd no-selection]}] [{:keys [exports title cmd no-selection]}]

View file

@ -20,7 +20,7 @@
[app.util.timers :as ts] [app.util.timers :as ts]
[beicon.core :as rx] [beicon.core :as rx]
[goog.functions :as f] [goog.functions :as f]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(defn use-id (defn use-id
"Get a stable id value across rerenders." "Get a stable id value across rerenders."
@ -29,7 +29,7 @@
(defn use-rxsub (defn use-rxsub
[ob] [ob]
(let [[state reset-state!] (mf/useState @ob)] (let [[state reset-state!] (mf/useState #(if (satisfies? IDeref ob) @ob nil))]
(mf/useEffect (mf/useEffect
(fn [] (fn []
(let [sub (rx/subscribe ob #(reset-state! %))] (let [sub (rx/subscribe ob #(reset-state! %))]
@ -313,3 +313,39 @@
(use-stream stream (partial reset! state)) (use-stream stream (partial reset! state))
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))

View file

@ -8,7 +8,7 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.logging :as log] [app.common.logging :as log]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(log/set-level! :warn) (log/set-level! :warn)

View file

@ -13,7 +13,7 @@
[app.main.ui.hooks :as hooks] [app.main.ui.hooks :as hooks]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.storage :refer [storage]] [app.util.storage :refer [storage]]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(log/set-level! :warn) (log/set-level! :warn)

View file

@ -5,13 +5,13 @@
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.main.ui.icons (ns app.main.ui.icons
(:require [rumext.alpha])) (:require [rumext.v2]))
(defmacro icon-xref (defmacro icon-xref
[id] [id]
(let [href (str "#icon-" (name id)) (let [href (str "#icon-" (name id))
class (str "icon-" (name id))] class (str "icon-" (name id))]
`(rumext.alpha/html `(rumext.v2/html
[:svg {:width 500 :height 500 :class ~class} [:svg {:width 500 :height 500 :class ~class}
[:use {:href ~href}]]))) [:use {:href ~href}]])))

View file

@ -7,7 +7,7 @@
(ns app.main.ui.icons (ns app.main.ui.icons
(:refer-clojure :exclude [import mask]) (:refer-clojure :exclude [import mask])
(:require-macros [app.main.ui.icons :refer [icon-xref]]) (: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 ;; Keep the list of icons sorted

View file

@ -8,7 +8,7 @@
(:require (:require
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
;; --- Component ;; --- Component

View file

@ -13,7 +13,7 @@
[app.common.math :as mth] [app.common.math :as mth]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.ui.formats :as fmt] [app.main.ui.formats :as fmt]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
;; ------------------------------------------------ ;; ------------------------------------------------
;; CONSTANTS ;; CONSTANTS

View file

@ -12,7 +12,7 @@
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[rumext.alpha :as mf])) [rumext.v2 :as mf]))
(mf/defc banner (mf/defc banner
[{:keys [type position status controls content actions on-close data-test] :as props}] [{: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