mirror of
https://github.com/penpot/penpot.git
synced 2025-06-09 21:31:37 +02:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
7303d311d5
283 changed files with 1548 additions and 1252 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
(db/exec-one! conn ["select count(*) as count from server_prop;"])
|
(try
|
||||||
(yrs/response 200 "OK")))
|
(db/exec-one! conn ["select count(*) as count from server_prop;"])
|
||||||
|
(yrs/response 200 "OK")
|
||||||
|
(catch Throwable cause
|
||||||
|
(l/warn :hint "unable to execute query on health handler"
|
||||||
|
:cause cause)
|
||||||
|
(yrs/response 503 "KO")))))
|
||||||
|
|
||||||
(defn changelog-handler
|
(defn changelog-handler
|
||||||
[_ _]
|
[_ _]
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -1,170 +0,0 @@
|
||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns app.loggers.sentry
|
|
||||||
"A mattermost integration for error reporting."
|
|
||||||
(:require
|
|
||||||
[app.common.logging :as l]
|
|
||||||
[app.common.uuid :as uuid]
|
|
||||||
[app.config :as cf]
|
|
||||||
[app.db :as db]
|
|
||||||
[app.util.async :as aa]
|
|
||||||
[app.worker :as wrk]
|
|
||||||
[clojure.core.async :as a]
|
|
||||||
[clojure.spec.alpha :as s]
|
|
||||||
[cuerdas.core :as str]
|
|
||||||
[integrant.core :as ig])
|
|
||||||
(:import
|
|
||||||
io.sentry.Scope
|
|
||||||
io.sentry.IHub
|
|
||||||
io.sentry.Hub
|
|
||||||
io.sentry.NoOpHub
|
|
||||||
io.sentry.protocol.User
|
|
||||||
io.sentry.SentryOptions
|
|
||||||
io.sentry.SentryLevel
|
|
||||||
io.sentry.ScopeCallback))
|
|
||||||
|
|
||||||
(defonce enabled (atom true))
|
|
||||||
|
|
||||||
(defn- parse-context
|
|
||||||
[event]
|
|
||||||
(reduce-kv
|
|
||||||
(fn [acc k v]
|
|
||||||
(cond
|
|
||||||
(= k :id) (assoc acc k (uuid/uuid v))
|
|
||||||
(= k :profile-id) (assoc acc k (uuid/uuid v))
|
|
||||||
(str/blank? v) acc
|
|
||||||
:else (assoc acc k v)))
|
|
||||||
{}
|
|
||||||
(:context event)))
|
|
||||||
|
|
||||||
(defn- parse-event
|
|
||||||
[event]
|
|
||||||
(assoc event :context (parse-context event)))
|
|
||||||
|
|
||||||
(defn- build-sentry-options
|
|
||||||
[cfg]
|
|
||||||
(let [version (:base cf/version)]
|
|
||||||
(doto (SentryOptions.)
|
|
||||||
(.setDebug (:debug cfg false))
|
|
||||||
(.setTracesSampleRate (:traces-sample-rate cfg 1.0))
|
|
||||||
(.setDsn (:dsn cfg))
|
|
||||||
(.setServerName (cf/get :host))
|
|
||||||
(.setEnvironment (cf/get :tenant))
|
|
||||||
(.setAttachServerName true)
|
|
||||||
(.setAttachStacktrace (:attach-stack-trace cfg false))
|
|
||||||
(.setRelease (str "backend@" (if (= version "0.0.0") "develop" version))))))
|
|
||||||
|
|
||||||
(defn handle-event
|
|
||||||
[^IHub shub event]
|
|
||||||
(letfn [(set-user! [^Scope scope {:keys [context] :as event}]
|
|
||||||
(let [user (User.)]
|
|
||||||
(.setIpAddress ^User user ^String (:ip-addr context))
|
|
||||||
(when-let [pid (:profile-id context)]
|
|
||||||
(.setId ^User user ^String (str pid)))
|
|
||||||
(.setUser scope ^User user)))
|
|
||||||
|
|
||||||
(set-level! [^Scope scope]
|
|
||||||
(.setLevel scope SentryLevel/ERROR))
|
|
||||||
|
|
||||||
(set-context! [^Scope scope {:keys [context] :as event}]
|
|
||||||
(let [uri (str (cf/get :public-uri) "/dbg/error-by-id/" (:id context))]
|
|
||||||
(.setContexts scope "detailed_error_uri" ^String uri))
|
|
||||||
(when-let [vers (:frontend-version event)]
|
|
||||||
(.setContexts scope "frontend_version" ^String vers))
|
|
||||||
(when-let [puri (:public-uri event)]
|
|
||||||
(.setContexts scope "public_uri" ^String (str puri)))
|
|
||||||
(when-let [uagent (:user-agent context)]
|
|
||||||
(.setContexts scope "user_agent" ^String uagent))
|
|
||||||
(when-let [tenant (:tenant event)]
|
|
||||||
(.setTag scope "tenant" ^String tenant))
|
|
||||||
(when-let [type (:error-type context)]
|
|
||||||
(.setTag scope "error_type" ^String (str type)))
|
|
||||||
(when-let [code (:error-code context)]
|
|
||||||
(.setTag scope "error_code" ^String (str code)))
|
|
||||||
)
|
|
||||||
|
|
||||||
(capture [^Scope scope {:keys [context error] :as event}]
|
|
||||||
(let [msg (str (:message error) "\n\n"
|
|
||||||
|
|
||||||
"======================================================\n"
|
|
||||||
"=================== Params ===========================\n"
|
|
||||||
"======================================================\n"
|
|
||||||
|
|
||||||
(:params context) "\n"
|
|
||||||
|
|
||||||
(when (:explain context)
|
|
||||||
(str "======================================================\n"
|
|
||||||
"=================== Explain ==========================\n"
|
|
||||||
"======================================================\n"
|
|
||||||
(:explain context) "\n"))
|
|
||||||
|
|
||||||
(when (:data context)
|
|
||||||
(str "======================================================\n"
|
|
||||||
"=================== Error Data =======================\n"
|
|
||||||
"======================================================\n"
|
|
||||||
(:data context) "\n"))
|
|
||||||
|
|
||||||
(str "======================================================\n"
|
|
||||||
"=================== Stack Trace ======================\n"
|
|
||||||
"======================================================\n"
|
|
||||||
(:trace error))
|
|
||||||
|
|
||||||
"\n")]
|
|
||||||
(set-user! scope event)
|
|
||||||
(set-level! scope)
|
|
||||||
(set-context! scope event)
|
|
||||||
(.captureMessage ^IHub shub msg)
|
|
||||||
))
|
|
||||||
]
|
|
||||||
(when @enabled
|
|
||||||
(.withScope ^IHub shub (reify ScopeCallback
|
|
||||||
(run [_ scope]
|
|
||||||
(->> event
|
|
||||||
(parse-event)
|
|
||||||
(capture scope))))))
|
|
||||||
|
|
||||||
))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
;; Error Listener
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
|
|
||||||
(s/def ::receiver any?)
|
|
||||||
(s/def ::dsn ::cf/sentry-dsn)
|
|
||||||
(s/def ::trace-sample-rate ::cf/sentry-trace-sample-rate)
|
|
||||||
(s/def ::attach-stack-trace ::cf/sentry-attach-stack-trace)
|
|
||||||
(s/def ::debug ::cf/sentry-debug)
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::reporter [_]
|
|
||||||
(s/keys :req-un [::wrk/executor ::db/pool ::receiver]
|
|
||||||
:opt-un [::dsn ::trace-sample-rate ::attach-stack-trace]))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::reporter
|
|
||||||
[_ {:keys [receiver dsn executor] :as cfg}]
|
|
||||||
(l/info :msg "initializing sentry reporter" :dsn dsn)
|
|
||||||
(let [opts (build-sentry-options cfg)
|
|
||||||
shub (if dsn
|
|
||||||
(Hub. ^SentryOptions opts)
|
|
||||||
(NoOpHub/getInstance))
|
|
||||||
output (a/chan (a/sliding-buffer 128)
|
|
||||||
(filter #(= (:level %) "error")))]
|
|
||||||
(receiver :sub output)
|
|
||||||
(a/go-loop []
|
|
||||||
(let [event (a/<! output)]
|
|
||||||
(if (nil? event)
|
|
||||||
(do
|
|
||||||
(l/info :msg "stoping error reporting loop")
|
|
||||||
(.close ^IHub shub))
|
|
||||||
(do
|
|
||||||
(a/<! (aa/with-thread executor (handle-event shub event)))
|
|
||||||
(recur)))))
|
|
||||||
output))
|
|
||||||
|
|
||||||
(defmethod ig/halt-key! ::reporter
|
|
||||||
[_ output]
|
|
||||||
(when output
|
|
||||||
(a/close! output)))
|
|
|
@ -244,6 +244,9 @@
|
||||||
|
|
||||||
{:name "0078-mod-file-media-object-table-drop-cascade"
|
{: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")}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE profile
|
||||||
|
ADD COLUMN is_blocked boolean DEFAULT false;
|
|
@ -31,9 +31,10 @@
|
||||||
|
|
||||||
(defn- handle-response-transformation
|
(defn- handle-response-transformation
|
||||||
[response request mdata]
|
[response request mdata]
|
||||||
(if-let [transform-fn (:transform-response mdata)]
|
(let [response (if (sv/wrapped? response) @response response)]
|
||||||
(p/do (transform-fn request response))
|
(if-let [transform-fn (:transform-response mdata)]
|
||||||
(p/resolved response)))
|
(p/do (transform-fn request 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
|
||||||
|
|
|
@ -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
|
||||||
:password (:password params)
|
(dt/duration "15m"))
|
||||||
:invitation-token (:invitation-token params)
|
|
||||||
:backend "penpot"
|
(defn- elapsed-register-retry-threshold?
|
||||||
:iss :prepared-register
|
[profile]
|
||||||
:exp (dt/in-future "48h")}
|
(let [elapsed (dt/diff (:modified-at profile) (dt/now))]
|
||||||
|
(pos? (compare elapsed register-retry-threshold))))
|
||||||
|
|
||||||
|
(defn prepare-register
|
||||||
|
[{:keys [pool sprops] :as cfg} params]
|
||||||
|
|
||||||
|
(validate-register-attempt! cfg params)
|
||||||
|
|
||||||
|
(let [profile (when-let [profile (profile/retrieve-profile-data-by-email pool (:email params))]
|
||||||
|
(cond
|
||||||
|
(:is-blocked profile)
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :profile-blocked)
|
||||||
|
|
||||||
|
(and (not (:is-active profile))
|
||||||
|
(elapsed-register-retry-threshold? profile))
|
||||||
|
profile
|
||||||
|
|
||||||
|
:else
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-already-exists
|
||||||
|
:hint "profile already exists")))
|
||||||
|
|
||||||
|
params {:email (:email params)
|
||||||
|
:password (:password params)
|
||||||
|
:invitation-token (:invitation-token params)
|
||||||
|
:backend "penpot"
|
||||||
|
:iss :prepared-register
|
||||||
|
:profile-id (:id profile)
|
||||||
|
:exp (dt/in-future {:days 7})}
|
||||||
|
|
||||||
|
params (d/without-nils params)
|
||||||
|
|
||||||
token (tokens/generate sprops params)]
|
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
|
||||||
|
@ -320,62 +355,76 @@
|
||||||
|
|
||||||
(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)
|
|
||||||
(not (contains? cf/flags :email-verification))
|
|
||||||
|
|
||||||
;; DEPRECATED: v1.15
|
is-active (or (:is-active params)
|
||||||
(contains? cf/flags :insecure-register))
|
(not (contains? cf/flags :email-verification))
|
||||||
|
|
||||||
profile (->> (assoc params :is-active is-active)
|
;; DEPRECATED: v1.15
|
||||||
|
(contains? cf/flags :insecure-register))
|
||||||
|
|
||||||
|
profile (if-let [profile-id (:profile-id claims)]
|
||||||
|
(profile/retrieve-profile conn profile-id)
|
||||||
|
(->> (assoc params :is-active is-active)
|
||||||
(create-profile conn)
|
(create-profile 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}))]
|
||||||
(cond
|
|
||||||
;; If invitation token comes in params, this is because the
|
|
||||||
;; user comes from team-invitation process; in this case,
|
|
||||||
;; regenerate token and send back to the user a new invitation
|
|
||||||
;; token (and mark current session as logged). This happens
|
|
||||||
;; only if the invitation email matches with the register
|
|
||||||
;; email.
|
|
||||||
(and (some? invitation) (= (:email profile) (:member-email invitation)))
|
|
||||||
(let [claims (assoc invitation :member-id (:id profile))
|
|
||||||
token (tokens/generate sprops claims)
|
|
||||||
resp {:invitation-token token}]
|
|
||||||
(with-meta resp
|
|
||||||
{:transform-response ((:create session) (:id profile))
|
|
||||||
::audit/replace-props (audit/profile->props profile)
|
|
||||||
::audit/profile-id (:id profile)}))
|
|
||||||
|
|
||||||
;; If auth backend is different from "penpot" means user is
|
;; If profile is filled in claims, means it tries to register
|
||||||
;; registering using third party auth mechanism; in this case
|
;; again, so we proceed to update the modified-at attr
|
||||||
;; we need to mark this session as logged.
|
;; accordingly.
|
||||||
(not= "penpot" (:auth-backend profile))
|
(when-let [id (:profile-id claims)]
|
||||||
(with-meta (profile/strip-private-attrs profile)
|
(db/update! conn :profile {:modified-at (dt/now)} {:id id})
|
||||||
|
(audit-fn :cmd :submit
|
||||||
|
:type "fact"
|
||||||
|
:name "register-profile-retry"
|
||||||
|
:profile-id id))
|
||||||
|
|
||||||
|
(cond
|
||||||
|
;; If invitation token comes in params, this is because the
|
||||||
|
;; user comes from team-invitation process; in this case,
|
||||||
|
;; regenerate token and send back to the user a new invitation
|
||||||
|
;; token (and mark current session as logged). This happens
|
||||||
|
;; only if the invitation email matches with the register
|
||||||
|
;; email.
|
||||||
|
(and (some? invitation) (= (:email profile) (:member-email invitation)))
|
||||||
|
(let [claims (assoc invitation :member-id (:id profile))
|
||||||
|
token (tokens/generate sprops claims)
|
||||||
|
resp {:invitation-token token}]
|
||||||
|
(with-meta resp
|
||||||
{:transform-response ((:create session) (:id profile))
|
{:transform-response ((:create session) (:id 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)}))
|
||||||
|
|
||||||
;; If the `:enable-insecure-register` flag is set, we proceed
|
;; If auth backend is different from "penpot" means user is
|
||||||
;; to sign in the user directly, without email verification.
|
;; registering using third party auth mechanism; in this case
|
||||||
(true? is-active)
|
;; we need to mark this session as logged.
|
||||||
(with-meta (profile/strip-private-attrs profile)
|
(not= "penpot" (:auth-backend profile))
|
||||||
{:transform-response ((:create session) (:id profile))
|
(with-meta (profile/strip-private-attrs profile)
|
||||||
::audit/replace-props (audit/profile->props profile)
|
{:transform-response ((:create session) (:id profile))
|
||||||
::audit/profile-id (:id profile)})
|
::audit/replace-props (audit/profile->props profile)
|
||||||
|
::audit/profile-id (:id profile)})
|
||||||
|
|
||||||
;; In all other cases, send a verification email.
|
;; If the `:enable-insecure-register` flag is set, we proceed
|
||||||
:else
|
;; to sign in the user directly, without email verification.
|
||||||
(do
|
(true? is-active)
|
||||||
(send-email-verification! conn sprops profile)
|
(with-meta (profile/strip-private-attrs profile)
|
||||||
(with-meta profile
|
{:transform-response ((:create session) (:id 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)})
|
||||||
|
|
||||||
|
;; In all other cases, send a verification email.
|
||||||
|
:else
|
||||||
|
(do
|
||||||
|
(send-email-verification! conn sprops profile)
|
||||||
|
(with-meta profile
|
||||||
|
{::audit/replace-props (audit/profile->props profile)
|
||||||
|
::audit/profile-id (:id profile)})))))
|
||||||
|
|
||||||
(s/def ::register-profile
|
(s/def ::register-profile
|
||||||
(s/keys :req-un [::token ::fullname]))
|
(s/keys :req-un [::token ::fullname]))
|
||||||
|
|
|
@ -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,
|
||||||
|
|
193
backend/src/app/rpc/commands/verify_token.clj
Normal file
193
backend/src/app/rpc/commands/verify_token.clj
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.rpc.commands.verify-token
|
||||||
|
(:require
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.loggers.audit :as audit]
|
||||||
|
[app.rpc.doc :as-alias doc]
|
||||||
|
[app.rpc.mutations.teams :as teams]
|
||||||
|
[app.rpc.queries.profile :as profile]
|
||||||
|
[app.tokens :as tokens]
|
||||||
|
[app.tokens.spec.team-invitation :as-alias spec.team-invitation]
|
||||||
|
[app.util.services :as sv]
|
||||||
|
[clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
|
(s/def ::iss keyword?)
|
||||||
|
(s/def ::exp ::us/inst)
|
||||||
|
|
||||||
|
(defmulti process-token (fn [_ _ claims] (:iss claims)))
|
||||||
|
|
||||||
|
(s/def ::verify-token
|
||||||
|
(s/keys :req-un [::token]
|
||||||
|
:opt-un [::profile-id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::verify-token
|
||||||
|
{:auth false
|
||||||
|
::doc/added "1.15"}
|
||||||
|
[{:keys [pool sprops] :as cfg} {:keys [token] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(let [claims (tokens/verify sprops {:token token})
|
||||||
|
cfg (assoc cfg :conn conn)]
|
||||||
|
(process-token cfg params claims))))
|
||||||
|
|
||||||
|
(defmethod process-token :change-email
|
||||||
|
[{:keys [conn] :as cfg} _params {:keys [profile-id email] :as claims}]
|
||||||
|
(when (profile/retrieve-profile-data-by-email conn email)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-already-exists))
|
||||||
|
|
||||||
|
(db/update! conn :profile
|
||||||
|
{:email email}
|
||||||
|
{:id profile-id})
|
||||||
|
|
||||||
|
(with-meta claims
|
||||||
|
{::audit/name "update-profile-email"
|
||||||
|
::audit/props {:email email}
|
||||||
|
::audit/profile-id profile-id}))
|
||||||
|
|
||||||
|
(defmethod process-token :verify-email
|
||||||
|
[{:keys [conn session] :as cfg} _ {:keys [profile-id] :as claims}]
|
||||||
|
(let [profile (profile/retrieve-profile conn profile-id)
|
||||||
|
claims (assoc claims :profile profile)]
|
||||||
|
|
||||||
|
(when-not (:is-active profile)
|
||||||
|
(when (not= (:email profile)
|
||||||
|
(:email claims))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :invalid-token))
|
||||||
|
|
||||||
|
(db/update! conn :profile
|
||||||
|
{:is-active true}
|
||||||
|
{:id (:id profile)}))
|
||||||
|
|
||||||
|
(with-meta claims
|
||||||
|
{:transform-response ((:create session) profile-id)
|
||||||
|
::audit/name "verify-profile-email"
|
||||||
|
::audit/props (audit/profile->props profile)
|
||||||
|
::audit/profile-id (:id profile)})))
|
||||||
|
|
||||||
|
(defmethod process-token :auth
|
||||||
|
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||||
|
(let [profile (profile/retrieve-profile conn profile-id)]
|
||||||
|
(assoc claims :profile profile)))
|
||||||
|
|
||||||
|
;; --- Team Invitation
|
||||||
|
|
||||||
|
(defn- accept-invitation
|
||||||
|
[{:keys [conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member]
|
||||||
|
(let [;; Update the role if there is an invitation
|
||||||
|
role (or (some-> invitation :role keyword) role)
|
||||||
|
params (merge
|
||||||
|
{:team-id team-id
|
||||||
|
:profile-id (:id member)}
|
||||||
|
(teams/role->params role))]
|
||||||
|
|
||||||
|
;; Do not allow blocked users accept invitations.
|
||||||
|
(when (:is-blocked member)
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :profile-blocked))
|
||||||
|
|
||||||
|
;; Insert the invited member to the team
|
||||||
|
(db/insert! conn :team-profile-rel params {:on-conflict-do-nothing true})
|
||||||
|
|
||||||
|
;; If profile is not yet verified, mark it as verified because
|
||||||
|
;; accepting an invitation link serves as verification.
|
||||||
|
(when-not (:is-active member)
|
||||||
|
(db/update! conn :profile
|
||||||
|
{:is-active true}
|
||||||
|
{:id (:id member)}))
|
||||||
|
|
||||||
|
;; Delete the invitation
|
||||||
|
(db/delete! conn :team-invitation
|
||||||
|
{:team-id team-id :email-to member-email})
|
||||||
|
|
||||||
|
(assoc member :is-active true)))
|
||||||
|
|
||||||
|
(s/def ::spec.team-invitation/profile-id ::us/uuid)
|
||||||
|
(s/def ::spec.team-invitation/role ::us/keyword)
|
||||||
|
(s/def ::spec.team-invitation/team-id ::us/uuid)
|
||||||
|
(s/def ::spec.team-invitation/member-email ::us/email)
|
||||||
|
(s/def ::spec.team-invitation/member-id (s/nilable ::us/uuid))
|
||||||
|
|
||||||
|
(s/def ::team-invitation-claims
|
||||||
|
(s/keys :req-un [::iss ::exp
|
||||||
|
::spec.team-invitation/profile-id
|
||||||
|
::spec.team-invitation/role
|
||||||
|
::spec.team-invitation/team-id
|
||||||
|
::spec.team-invitation/member-email]
|
||||||
|
:opt-un [::spec.team-invitation/member-id]))
|
||||||
|
|
||||||
|
(defmethod process-token :team-invitation
|
||||||
|
[{:keys [conn session] :as cfg} {:keys [profile-id token]}
|
||||||
|
{:keys [member-id team-id member-email] :as claims}]
|
||||||
|
|
||||||
|
(us/assert ::team-invitation-claims claims)
|
||||||
|
|
||||||
|
(let [invitation (db/get* conn :team-invitation
|
||||||
|
{:team-id team-id :email-to member-email})
|
||||||
|
profile (db/get* conn :profile
|
||||||
|
{:id profile-id}
|
||||||
|
{:columns [:id :email]})]
|
||||||
|
(when (nil? invitation)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :invalid-token
|
||||||
|
:hint "no invitation associated with the token"))
|
||||||
|
|
||||||
|
(if (some? profile)
|
||||||
|
(if (or (= member-id profile-id)
|
||||||
|
(= member-email (:email profile)))
|
||||||
|
;; if we have logged-in user and it matches the invitation we
|
||||||
|
;; proceed with accepting the invitation and joining the
|
||||||
|
;; current profile to the invited team.
|
||||||
|
(let [profile (accept-invitation cfg claims invitation profile)]
|
||||||
|
(with-meta
|
||||||
|
(assoc claims :state :created)
|
||||||
|
{::audit/name "accept-team-invitation"
|
||||||
|
::audit/props (merge
|
||||||
|
(audit/profile->props profile)
|
||||||
|
{:team-id (:team-id claims)
|
||||||
|
:role (:role claims)})
|
||||||
|
::audit/profile-id profile-id}))
|
||||||
|
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :invalid-token
|
||||||
|
:hint "logged-in user does not matches the invitation"))
|
||||||
|
|
||||||
|
;; If we have not logged-in user, we try find the invited
|
||||||
|
;; profile by member-id or member-email props of the invitation
|
||||||
|
;; token; If profile is found, we accept the invitation and
|
||||||
|
;; leave the user logged-in.
|
||||||
|
(if-let [member (db/get* conn :profile
|
||||||
|
(if member-id
|
||||||
|
{:id member-id}
|
||||||
|
{:email member-email})
|
||||||
|
{:columns [:id :email]})]
|
||||||
|
(let [profile (accept-invitation cfg claims invitation member)]
|
||||||
|
(with-meta
|
||||||
|
(assoc claims :state :created)
|
||||||
|
{:transform-response ((:create session) (:id profile))
|
||||||
|
::audit/name "accept-team-invitation"
|
||||||
|
::audit/props (merge
|
||||||
|
(audit/profile->props profile)
|
||||||
|
{:team-id (:team-id claims)
|
||||||
|
:role (:role claims)})
|
||||||
|
::audit/profile-id member-id}))
|
||||||
|
|
||||||
|
{:invitation-token token
|
||||||
|
:iss :team-invitation
|
||||||
|
:redirect-to :auth-register
|
||||||
|
:state :pending}))))
|
||||||
|
|
||||||
|
;; --- Default
|
||||||
|
|
||||||
|
(defmethod process-token :default
|
||||||
|
[_ _ _]
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :invalid-token))
|
||||||
|
|
|
@ -565,6 +565,8 @@
|
||||||
(s/keys :req-un [::profile-id ::file-id ::revn ::data ::props]))
|
(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)
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
||||||
(db/update! conn :profile
|
;; If we found owned teams with participants, we don't allow
|
||||||
{:deleted-at (dt/now)}
|
;; delete profile until the user properly transfer ownership or
|
||||||
{:id profile-id})
|
;; explicitly removes all participants from the team
|
||||||
|
(when (some pos? (map :participants teams))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :owner-teams-with-people
|
||||||
|
:hint "The user need to transfer ownership of owned teams."
|
||||||
|
:context {:teams (mapv :id teams)}))
|
||||||
|
|
||||||
(with-meta {}
|
(doseq [{:keys [id]} teams]
|
||||||
{:transform-response (:delete session)})))
|
(db/update! conn :team
|
||||||
|
{:deleted-at deleted-at}
|
||||||
|
{:id id}))
|
||||||
|
|
||||||
|
(db/update! conn :profile
|
||||||
|
{:deleted-at deleted-at}
|
||||||
|
{:id profile-id})
|
||||||
|
|
||||||
|
(with-meta {}
|
||||||
|
{:transform-response (:delete session)}))))
|
||||||
|
|
||||||
(def sql:owned-teams
|
(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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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))
|
|
||||||
|
|
||||||
|
|
|
@ -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))))
|
||||||
|
|
|
@ -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,22 +147,23 @@
|
||||||
(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.
|
(db/with-atomic [conn pool]
|
||||||
(let [min-age (or (:min-age params) (:min-age cfg))]
|
(let [min-age (or (:min-age params) (:min-age cfg))
|
||||||
(db/with-atomic [conn pool]
|
cfg (-> cfg
|
||||||
(let [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)
|
total 0]
|
||||||
total 0]
|
(if-let [table (first tables)]
|
||||||
(if-let [table (first tables)]
|
(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 "objects gc finished succesfully"
|
||||||
(l/info :hint "task finished" :min-age (dt/format-duration min-age) :total total)
|
: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})))))))
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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))))))
|
||||||
|
|
|
@ -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))))
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)]
|
||||||
(let [data {::th/type :prepare-register-profile
|
(t/is (th/success? out))
|
||||||
:email "user@example.com"
|
(let [result (:result out)]
|
||||||
:password "USER@example.com"}]
|
(t/is (contains? result :token))))))
|
||||||
|
|
||||||
(let [{:keys [result error] :as out} (th/mutation! data)]
|
(t/deftest register-profile-with-email-as-password
|
||||||
(t/is (th/ex-info? error))
|
(let [data {::th/type :prepare-register-profile
|
||||||
(t/is (th/ex-of-type? error :validation))
|
:email "user@example.com"
|
||||||
(t/is (th/ex-of-code? error :email-as-password)))))
|
:password "USER@example.com"}
|
||||||
|
out (th/command! data)]
|
||||||
|
|
||||||
(t/deftest test-email-change-request
|
(t/is (not (th/success? out)))
|
||||||
(with-mocks [email-send-mock {:target 'app.emails/send! :return nil}]
|
(let [edata (-> out :error ex-data)]
|
||||||
|
(t/is (= :validation (:type edata)))
|
||||||
|
(t/is (= :email-as-password (:code edata))))))
|
||||||
|
|
||||||
|
(t/deftest email-change-request
|
||||||
|
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
||||||
(let [profile (th/create-profile* 1)
|
(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)
|
;; (th/print-result! out)
|
||||||
res (:result out)]
|
(t/is (false? (:called? @mock)))
|
||||||
|
(let [res (:result out)]
|
||||||
;; (th/print-result! out)
|
(t/is (= {:changed true} res)))))))
|
||||||
(t/is (= {:changed true} res))
|
|
||||||
(let [mock (deref email-send-mock)]
|
|
||||||
(t/is (false? (:called? mock)))))))))
|
|
||||||
|
|
||||||
|
|
||||||
(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)))
|
||||||
|
|
|
@ -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})
|
||||||
|
@ -34,17 +35,16 @@
|
||||||
:profile-id (:id profile1)}]
|
:profile-id (:id profile1)}]
|
||||||
|
|
||||||
;; invite external user without complaints
|
;; invite external user without complaints
|
||||||
(let [data (assoc data :email "foo@bar.com")
|
(let [data (assoc data :email "foo@bar.com")
|
||||||
out (th/mutation! data)
|
out (th/mutation! data)
|
||||||
;;retrieve the value from the database and check its content
|
;; retrieve the value from the database and check its content
|
||||||
invitation (db/exec-one!
|
invitation (db/exec-one!
|
||||||
th/*pool*
|
th/*pool*
|
||||||
["select count(*) as num from team_invitation where team_id = ? and email_to = ?"
|
["select count(*) as num from team_invitation where team_id = ? and email_to = ?"
|
||||||
(: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,31 +330,27 @@
|
||||||
: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)})
|
||||||
data {::th/type :team-invitations
|
data {::th/type :team-invitations
|
||||||
:profile-id (:id prof)
|
:profile-id (:id prof)
|
||||||
:team-id (:id team)}]
|
:team-id (:id team)}]
|
||||||
|
|
||||||
;;insert an entry on the database with an enabled invitation
|
;; insert an entry on the database with an enabled invitation
|
||||||
(db/insert! th/*pool* :team-invitation
|
(db/insert! th/*pool* :team-invitation
|
||||||
{:team-id (:team-id data)
|
{:team-id (:team-id data)
|
||||||
:email-to "test1@mail.com"
|
:email-to "test1@mail.com"
|
||||||
: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)
|
||||||
:email-to "test2@mail.com"
|
:email-to "test2@mail.com"
|
||||||
|
@ -219,27 +358,26 @@
|
||||||
: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)]
|
||||||
(t/is (= 2 (count result)))
|
(t/is (= 2 (count result)))
|
||||||
(t/is (= "test1@mail.com" (:email one)))
|
(t/is (= "test1@mail.com" (:email one)))
|
||||||
(t/is (= "test2@mail.com" (:email two)))
|
(t/is (= "test2@mail.com" (:email two)))
|
||||||
(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)})
|
||||||
data {::th/type :update-team-invitation-role
|
data {::th/type :update-team-invitation-role
|
||||||
:profile-id (:id prof)
|
:profile-id (:id prof)
|
||||||
:team-id (:id team)
|
:team-id (:id team)
|
||||||
:email "TEST1@mail.com"
|
:email "TEST1@mail.com"
|
||||||
:role :admin}]
|
:role :admin}]
|
||||||
|
|
||||||
;;insert an entry on the database with an invitation
|
;; insert an entry on the database with an invitation
|
||||||
(db/insert! th/*pool* :team-invitation
|
(db/insert! th/*pool* :team-invitation
|
||||||
{:team-id (:team-id data)
|
{:team-id (:team-id data)
|
||||||
:email-to "test1@mail.com"
|
:email-to "test1@mail.com"
|
||||||
|
@ -247,24 +385,22 @@
|
||||||
:valid-until (dt/in-future "48h")})
|
:valid-until (dt/in-future "48h")})
|
||||||
|
|
||||||
(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})
|
||||||
team (th/create-team* 1 {:profile-id (:id prof)})
|
team (th/create-team* 1 {:profile-id (:id prof)})
|
||||||
data {::th/type :delete-team-invitation
|
data {::th/type :delete-team-invitation
|
||||||
:profile-id (:id prof)
|
:profile-id (:id prof)
|
||||||
:team-id (:id team)
|
:team-id (:id team)
|
||||||
:email "TEST1@mail.com"}]
|
:email "TEST1@mail.com"}]
|
||||||
|
|
||||||
;;insert an entry on the database with an invitation
|
;; insert an entry on the database with an invitation
|
||||||
(db/insert! th/*pool* :team-invitation
|
(db/insert! th/*pool* :team-invitation
|
||||||
{:team-id (:team-id data)
|
{:team-id (:team-id data)
|
||||||
:email-to "test1@mail.com"
|
:email-to "test1@mail.com"
|
||||||
|
@ -272,10 +408,10 @@
|
||||||
:valid-until (dt/in-future "48h")})
|
:valid-until (dt/in-future "48h")})
|
||||||
|
|
||||||
(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)))))
|
||||||
|
|
|
@ -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
|
||||||
{:error nil
|
(let [result# (deref ~expr)
|
||||||
:result (deref ~expr)}
|
result# (cond-> result# (sv/wrapped? result#) deref)]
|
||||||
|
{:error nil
|
||||||
|
: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 {})))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}"; \
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"]}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
(js/console.error cause)))
|
(fn [cause]
|
||||||
|
(js/console.error cause)))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"}}}
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns app.main.sentry
|
|
||||||
"Sentry integration."
|
|
||||||
(:require
|
|
||||||
["@sentry/browser" :as sentry]
|
|
||||||
[app.common.exceptions :as ex]
|
|
||||||
[app.common.uuid :as uuid]
|
|
||||||
[app.config :as cf]
|
|
||||||
[app.main.refs :as refs]))
|
|
||||||
|
|
||||||
(defn- setup-profile!
|
|
||||||
[profile]
|
|
||||||
(if (or (= uuid/zero (:id profile))
|
|
||||||
(nil? profile))
|
|
||||||
(sentry/setUser nil)
|
|
||||||
(sentry/setUser #js {:id (str (:id profile))})))
|
|
||||||
|
|
||||||
(defn init!
|
|
||||||
[]
|
|
||||||
(setup-profile! @refs/profile)
|
|
||||||
(when cf/sentry-dsn
|
|
||||||
(sentry/init
|
|
||||||
#js {:dsn cf/sentry-dsn
|
|
||||||
:autoSessionTracking false
|
|
||||||
:attachStacktrace false
|
|
||||||
:release (str "frontend@" (:base @cf/version))
|
|
||||||
:maxBreadcrumbs 20
|
|
||||||
:beforeBreadcrumb (fn [breadcrumb _hint]
|
|
||||||
(let [category (.-category ^js breadcrumb)]
|
|
||||||
(if (= category "navigate")
|
|
||||||
breadcrumb
|
|
||||||
nil)))
|
|
||||||
:tracesSampleRate 1.0})
|
|
||||||
|
|
||||||
(add-watch refs/profile ::profile
|
|
||||||
(fn [_ _ _ profile]
|
|
||||||
(setup-profile! profile)))
|
|
||||||
|
|
||||||
(add-watch refs/route ::route
|
|
||||||
(fn [_ _ _ route]
|
|
||||||
(sentry/addBreadcrumb
|
|
||||||
#js {:category "navigate",
|
|
||||||
:message (str "path: " (:path route))
|
|
||||||
:level (.-Info ^js sentry/Severity)})))))
|
|
||||||
|
|
||||||
(defn capture-exception
|
|
||||||
[err]
|
|
||||||
(when cf/sentry-dsn
|
|
||||||
(when (ex/ex-info? err)
|
|
||||||
(sentry/setContext "ex-data", (clj->js (ex-data err))))
|
|
||||||
(sentry/captureException err))
|
|
||||||
err)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
[app.main.ui.viewer :as viewer]
|
[app.main.ui.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}]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
[]
|
[]
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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]))
|
||||||
|
|
|
@ -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"})
|
||||||
|
|
||||||
|
|
|
@ -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,33 +62,34 @@
|
||||||
[{: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 []
|
|
||||||
(dom/set-html-title (tr "title.default"))
|
|
||||||
(->> (rp/mutation :verify-token {:token token})
|
|
||||||
(rx/subs
|
|
||||||
(fn [tdata]
|
|
||||||
(handle-token tdata))
|
|
||||||
(fn [{:keys [type code] :as error}]
|
|
||||||
(cond
|
|
||||||
(or (= :validation type)
|
|
||||||
(= :invalid-token code)
|
|
||||||
(= :token-expired (:reason error)))
|
|
||||||
(reset! bad-token true)
|
|
||||||
(= :email-already-exists code)
|
|
||||||
(let [msg (tr "errors.email-already-exists")]
|
|
||||||
(ts/schedule 100 #(st/emit! (dm/error msg)))
|
|
||||||
(st/emit! (rt/nav :auth-login)))
|
|
||||||
|
|
||||||
(= :email-already-validated code)
|
(mf/with-effect []
|
||||||
(let [msg (tr "errors.email-already-validated")]
|
(dom/set-html-title (tr "title.default"))
|
||||||
(ts/schedule 100 #(st/emit! (dm/warn msg)))
|
(->> (rp/command! :verify-token {:token token})
|
||||||
(st/emit! (rt/nav :auth-login)))
|
(rx/subs
|
||||||
|
(fn [tdata]
|
||||||
|
(handle-token tdata))
|
||||||
|
(fn [{:keys [type code] :as error}]
|
||||||
|
(cond
|
||||||
|
(or (= :validation type)
|
||||||
|
(= :invalid-token code)
|
||||||
|
(= :token-expired (:reason error)))
|
||||||
|
(reset! bad-token true)
|
||||||
|
|
||||||
:else
|
(= :email-already-exists code)
|
||||||
(let [msg (tr "errors.generic")]
|
(let [msg (tr "errors.email-already-exists")]
|
||||||
(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)))
|
||||||
|
|
||||||
|
(= :email-already-validated code)
|
||||||
|
(let [msg (tr "errors.email-already-validated")]
|
||||||
|
(ts/schedule 100 #(st/emit! (dm/warn msg)))
|
||||||
|
(st/emit! (rt/nav :auth-login)))
|
||||||
|
|
||||||
|
:else
|
||||||
|
(let [msg (tr "errors.generic")]
|
||||||
|
(ts/schedule 100 #(st/emit! (dm/error msg)))
|
||||||
|
(st/emit! (rt/nav :auth-login))))))))
|
||||||
|
|
||||||
(if @bad-token
|
(if @bad-token
|
||||||
[:> static/static-header {}
|
[:> static/static-header {}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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}]
|
||||||
|
|
|
@ -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}]
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]}]
|
||||||
|
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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}]
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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,43 +20,57 @@
|
||||||
[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)
|
:file-name (:name file)
|
||||||
:components-v2 components-v2})))
|
: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])
|
||||||
(->> (ask-for-thumbnail file)
|
visible? (h/use-visible container :once? true)]
|
||||||
(rx/subs (fn [{:keys [data fonts] :as params}]
|
|
||||||
(run! fonts/ensure-loaded! fonts)
|
|
||||||
(when-let [node (mf/ref-val container)]
|
|
||||||
(dom/set-html! node data))))))
|
|
||||||
|
|
||||||
[:div.grid-item-th {:style {:background-color (get-in file [:data :options :background])}
|
(mf/with-effect [file visible?]
|
||||||
:ref container}
|
(when visible?
|
||||||
|
(let [tp (perf/tpoint)]
|
||||||
|
(->> (ask-for-thumbnail file)
|
||||||
|
(rx/subscribe-on :af)
|
||||||
|
(rx/subs (fn [{:keys [data fonts] :as params}]
|
||||||
|
(run! fonts/ensure-loaded! fonts)
|
||||||
|
(log/info :hint "loaded thumbnail"
|
||||||
|
:file-id (dm/str (:id file))
|
||||||
|
:file-name (:name file)
|
||||||
|
:elapsed (str/ffmt "%ms" (tp)))
|
||||||
|
(when-let [node (mf/ref-val container)]
|
||||||
|
(dom/set-html! node data))))))))
|
||||||
|
|
||||||
|
[:div.grid-item-th
|
||||||
|
{:style {:background-color bgcolor}
|
||||||
|
:ref container}
|
||||||
i/loader-pencil]))
|
i/loader-pencil]))
|
||||||
|
|
||||||
;; --- Grid Item Library
|
;; --- Grid Item Library
|
||||||
|
@ -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
|
||||||
|
@ -159,18 +175,19 @@
|
||||||
(mf/defc grid-item
|
(mf/defc grid-item
|
||||||
{:wrap [mf/memo]}
|
{:wrap [mf/memo]}
|
||||||
[{:keys [file navigate? origin library-view?] :as props}]
|
[{:keys [file navigate? origin library-view?] :as props}]
|
||||||
(let [file-id (:id file)
|
(let [file-id (:id file)
|
||||||
local (mf/use-state {:menu-open false
|
local (mf/use-state {:menu-open false
|
||||||
:menu-pos nil
|
:menu-pos nil
|
||||||
: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)
|
(when (and (not selected?) (:menu-open @local))
|
||||||
(fn []
|
(swap! local assoc :menu-open false)))
|
||||||
(when (and (not selected?) (:menu-open @local))
|
|
||||||
(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-over on-drag-over
|
{:on-drag-enter on-drag-enter
|
||||||
:on-drag-leave on-drag-leave
|
:on-drag-over on-drag-over
|
||||||
:on-drop on-drop}
|
:on-drag-leave on-drag-leave
|
||||||
|
: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,21 +382,21 @@
|
||||||
: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
|
||||||
:on-create-clicked on-create-clicked
|
{:default? (:is-default project)
|
||||||
:project project
|
:on-create-clicked on-create-clicked
|
||||||
:limit limit
|
:project project
|
||||||
:origin origin}])]))
|
:limit limit
|
||||||
|
:origin origin}])]))
|
||||||
|
|
||||||
(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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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}]
|
||||||
|
|
|
@ -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}]
|
||||||
|
|
|
@ -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}]
|
||||||
|
|
|
@ -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?)
|
||||||
|
|
|
@ -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
|
||||||
(fn []
|
(mf/use-fn
|
||||||
(swap! state #(assoc % :status :waiting))
|
(fn []
|
||||||
(st/emit!
|
(swap! state #(assoc % :status :waiting))
|
||||||
(dm/error (tr "dashboard.libraries-and-templates.import-error"))))
|
(st/emit!
|
||||||
|
(msg/error (tr "dashboard.libraries-and-templates.import-error")))))
|
||||||
|
|
||||||
download-tutorial
|
download-tutorial
|
||||||
(fn []
|
(mf/use-fn
|
||||||
(let [mdata {:on-success on-template-cloned-success :on-error on-template-cloned-error}
|
(mf/deps template default-project-id)
|
||||||
params {:project-id default-project-id :template-id (:id template)}]
|
(fn []
|
||||||
(swap! state #(assoc % :status :importing))
|
(let [mdata {:on-success on-template-cloned-success :on-error on-template-cloned-error}
|
||||||
(st/emit! (with-meta (dd/clone-template (with-meta params mdata))
|
params {:project-id default-project-id :template-id (:id template)}]
|
||||||
{::ev/origin "get-started-hero-block"}))))]
|
(swap! state #(assoc % :status :importing))
|
||||||
|
(st/emit! (with-meta (dd/clone-template (with-meta params mdata))
|
||||||
|
{::ev/origin "get-started-hero-block"})))))]
|
||||||
[:div.tutorial
|
[:div.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,32 +139,32 @@
|
||||||
[{: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)})
|
|
||||||
|
width (mf/use-state nil)
|
||||||
|
rowref (mf/use-ref)
|
||||||
|
itemsize (if (>= @width 1030)
|
||||||
|
280
|
||||||
|
230)
|
||||||
|
|
||||||
|
ratio (if (some? @width) (/ @width itemsize) 0)
|
||||||
|
nitems (mth/floor ratio)
|
||||||
|
limit (min 10 nitems)
|
||||||
|
limit (max 1 limit)
|
||||||
|
|
||||||
on-nav
|
on-nav
|
||||||
(mf/use-callback
|
(mf/use-fn
|
||||||
(mf/deps project)
|
(mf/deps project)
|
||||||
#(st/emit! (rt/nav :dashboard-files {:team-id (:team-id project)
|
(fn []
|
||||||
:project-id (:id project)})))
|
(st/emit! (rt/nav :dashboard-files {:team-id (:team-id project)
|
||||||
|
:project-id project-id}))))
|
||||||
width (mf/use-state nil)
|
|
||||||
rowref (mf/use-ref)
|
|
||||||
itemsize (if (>= @width 1030)
|
|
||||||
280
|
|
||||||
230)
|
|
||||||
|
|
||||||
ratio (if (some? @width) (/ @width itemsize) 0)
|
|
||||||
nitems (mth/floor ratio)
|
|
||||||
limit (min 10 nitems)
|
|
||||||
limit (max 1 limit)
|
|
||||||
|
|
||||||
toggle-pin
|
toggle-pin
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps project)
|
(mf/deps project)
|
||||||
|
@ -209,22 +220,24 @@
|
||||||
(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)
|
(rx/observe-on :af)
|
||||||
(rx/observe-on :af)
|
(rx/subs (fn [entries]
|
||||||
(rx/subs (fn [entries]
|
(let [row (first entries)
|
||||||
(let [row (first entries)
|
row-rect (.-contentRect ^js row)
|
||||||
row-rect (.-contentRect ^js row)
|
row-width (.-width ^js row-rect)]
|
||||||
row-width (.-width ^js row-rect)]
|
(when @mnt?
|
||||||
(when @mnt?
|
(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 []
|
||||||
(ptk/event ::ev/event {::ev/name "dont-show"
|
(st/emit! (du/update-profile-props {:team-hero? false})
|
||||||
::ev/origin "get-started-hero-block"
|
(ptk/event ::ev/event {::ev/name "dont-show-team-up-hero"
|
||||||
:type "tutorial"
|
::ev/origin "dashboard"}))))
|
||||||
:section "dashboard"})))
|
close-tutorial
|
||||||
|
(mf/use-fn
|
||||||
|
(fn []
|
||||||
|
(st/emit! (du/update-profile-props {:viewed-tutorial? true})
|
||||||
|
(ptk/event ::ev/event {::ev/name "dont-show"
|
||||||
|
::ev/origin "get-started-hero-block"
|
||||||
|
:type "tutorial"
|
||||||
|
:section "dashboard"}))))
|
||||||
|
close-walkthrough
|
||||||
|
(mf/use-fn
|
||||||
|
(fn []
|
||||||
|
(st/emit! (du/update-profile-props {:viewed-walkthrough? true})
|
||||||
|
(ptk/event ::ev/event {::ev/name "dont-show"
|
||||||
|
::ev/origin "get-started-hero-block"
|
||||||
|
:type "walkthrough"
|
||||||
|
:section "dashboard"}))))]
|
||||||
|
|
||||||
close-walkthrough (fn []
|
(mf/with-effect [team]
|
||||||
(st/emit!
|
(let [tname (if (:is-default team)
|
||||||
(du/update-profile-props {:viewed-walkthrough? true})
|
(tr "dashboard.your-penpot")
|
||||||
(ptk/event ::ev/event {::ev/name "dont-show"
|
(:name team))]
|
||||||
::ev/origin "get-started-hero-block"
|
(dom/set-html-title (tr "title.dashboard.projects" tname))))
|
||||||
:type "walkthrough"
|
|
||||||
:section "dashboard"})))]
|
|
||||||
|
|
||||||
(mf/use-effect
|
(mf/with-effect [team-id]
|
||||||
(mf/deps team)
|
(st/emit! (dd/fetch-recent-files team-id)
|
||||||
(fn []
|
(dd/clear-selected-files)))
|
||||||
(let [tname (if (:is-default team)
|
|
||||||
(tr "dashboard.your-penpot")
|
|
||||||
(:name team))]
|
|
||||||
(dom/set-html-title (tr "title.dashboard.projects" tname)))))
|
|
||||||
|
|
||||||
(mf/use-effect
|
|
||||||
(mf/deps (:id team))
|
|
||||||
(fn []
|
|
||||||
(st/emit! (dd/fetch-recent-files (:id team))
|
|
||||||
(dd/clear-selected-files))))
|
|
||||||
|
|
||||||
(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}]))]])))
|
||||||
|
|
||||||
|
|
|
@ -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}]
|
||||||
|
|
|
@ -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}]
|
||||||
|
|
|
@ -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]}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]}]
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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}]])))
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue