0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-13 10:38:13 -05:00

Merge pull request #2346 from penpot/niwinz-hot-improvements

Improvements & Fixes (part 2)
This commit is contained in:
Alejandro 2022-09-28 12:04:55 +02:00 committed by GitHub
commit 35a72be4f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
283 changed files with 1547 additions and 1251 deletions

View file

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

View file

@ -37,6 +37,7 @@
- Fix inconsistent message on deleting library when a library is linked from deleted files
- Fix change multiple colors with SVG [Taiga #3889](https://tree.taiga.io/project/penpot/issue/3889)
- Fix ungroup does not work for typographies [Taiga #4195](https://tree.taiga.io/project/penpot/issue/4195)
- Fix inviting to non existing users can fail [Taiga #4108](https://tree.taiga.io/project/penpot/issue/4108)
### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!)
@ -68,6 +69,7 @@
- Fix Terms and Privacy links overlapping [Taiga #4137](https://tree.taiga.io/project/penpot/issue/4137)
- Fix Export bounding box mask [Taiga #950](https://tree.taiga.io/project/penpot/issue/950)
- Fix delete layers in bulk [Taiga #4160](https://tree.taiga.io/project/penpot/issue/4160)
- Fix Cannot take out an element from a group at layers panel by drag [Taiga #4209](https://tree.taiga.io/project/penpot/issue/4209)
## 1.15.3-beta

View file

@ -6,26 +6,26 @@
;; Logging
org.zeromq/jeromq {:mvn/version "0.5.2"}
com.github.luben/zstd-jni {:mvn/version "1.5.2-3"}
com.github.luben/zstd-jni {:mvn/version "1.5.2-4"}
org.clojure/data.fressian {:mvn/version "1.0.0"}
io.prometheus/simpleclient {:mvn/version "0.15.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.15.0"}
io.prometheus/simpleclient_jetty {:mvn/version "0.15.0"
io.prometheus/simpleclient {:mvn/version "0.16.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
io.prometheus/simpleclient_jetty {:mvn/version "0.16.0"
:exclusions [org.eclipse.jetty/jetty-server
org.eclipse.jetty/jetty-servlet]}
io.prometheus/simpleclient_httpserver {:mvn/version "0.15.0"}
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
io.lettuce/lettuce-core {:mvn/version "6.1.8.RELEASE"}
io.lettuce/lettuce-core {:mvn/version "6.2.0.RELEASE"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/yetti {:git/tag "v9.8" :git/sha "fbe1d7d"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}
com.github.seancorfield/next.jdbc {:mvn/version "1.2.780"}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.828"}
metosin/reitit-core {:mvn/version "0.5.18"}
org.postgresql/postgresql {:mvn/version "42.4.0"}
org.postgresql/postgresql {:mvn/version "42.5.0"}
com.zaxxer/HikariCP {:mvn/version "5.0.1"}
io.whitfin/siphash {:mvn/version "2.0.0"}
@ -42,14 +42,12 @@
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
integrant/integrant {:mvn/version "0.8.0"}
io.sentry/sentry {:mvn/version "5.6.1"}
dawran6/emoji {:mvn/version "0.1.5"}
markdown-clj/markdown-clj {:mvn/version "1.11.1"}
markdown-clj/markdown-clj {:mvn/version "1.11.3"}
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.17.272"}}
software.amazon.awssdk/s3 {:mvn/version "2.17.278"}}
:paths ["src" "resources" "target/classes"]
:aliases
@ -65,8 +63,7 @@
:extra-paths ["test" "dev"]}
:build
{:extra-deps
{io.github.clojure/tools.build {:git/tag "v0.8.2" :git/sha "ba1a2bf"}}
{:extra-deps {io.github.clojure/tools.build {:git/tag "v0.8.3" :git/sha "0d20256"}}
:ns-default build}
:test

View file

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

View file

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

View file

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

View file

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

View file

@ -52,13 +52,6 @@
(let [mdata (meta obj)
backend (sto/resolve-backend storage (:backend obj))]
(case (:type backend)
:db
(p/let [body (sto/get-object-bytes storage obj)]
(yrs/response :status 200
:body body
:headers {"content-type" (:content-type mdata)
"cache-control" (str "max-age=" (inst-ms cache-max-age))}))
:s3
(p/let [{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age signature-max-age})]
(yrs/response :status 307

View file

@ -8,6 +8,7 @@
(:refer-clojure :exclude [error-handler])
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.pprint :as pp]
[app.common.uuid :as uuid]
[app.config :as cf]
@ -213,7 +214,7 @@
(render-template [report]
(let [context (dissoc report
:trace :cause :params :data :spec-problems
:trace :cause :params :data :spec-problems :message
:spec-explain :spec-value :error :explain :hint)
params {:context (pp/pprint-str context :width 200)
:hint (:hint report)
@ -341,8 +342,13 @@
"Mainly a task that performs a health check."
[{:keys [pool]} _]
(db/with-atomic [conn pool]
(db/exec-one! conn ["select count(*) as count from server_prop;"])
(yrs/response 200 "OK")))
(try
(db/exec-one! conn ["select count(*) as count from server_prop;"])
(yrs/response 200 "OK")
(catch Throwable cause
(l/warn :hint "unable to execute query on health handler"
:cause cause)
(yrs/response 503 "KO")))))
(defn changelog-handler
[_ _]

View file

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

View file

@ -1,170 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.loggers.sentry
"A mattermost integration for error reporting."
(:require
[app.common.logging :as l]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.util.async :as aa]
[app.worker :as wrk]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig])
(:import
io.sentry.Scope
io.sentry.IHub
io.sentry.Hub
io.sentry.NoOpHub
io.sentry.protocol.User
io.sentry.SentryOptions
io.sentry.SentryLevel
io.sentry.ScopeCallback))
(defonce enabled (atom true))
(defn- parse-context
[event]
(reduce-kv
(fn [acc k v]
(cond
(= k :id) (assoc acc k (uuid/uuid v))
(= k :profile-id) (assoc acc k (uuid/uuid v))
(str/blank? v) acc
:else (assoc acc k v)))
{}
(:context event)))
(defn- parse-event
[event]
(assoc event :context (parse-context event)))
(defn- build-sentry-options
[cfg]
(let [version (:base cf/version)]
(doto (SentryOptions.)
(.setDebug (:debug cfg false))
(.setTracesSampleRate (:traces-sample-rate cfg 1.0))
(.setDsn (:dsn cfg))
(.setServerName (cf/get :host))
(.setEnvironment (cf/get :tenant))
(.setAttachServerName true)
(.setAttachStacktrace (:attach-stack-trace cfg false))
(.setRelease (str "backend@" (if (= version "0.0.0") "develop" version))))))
(defn handle-event
[^IHub shub event]
(letfn [(set-user! [^Scope scope {:keys [context] :as event}]
(let [user (User.)]
(.setIpAddress ^User user ^String (:ip-addr context))
(when-let [pid (:profile-id context)]
(.setId ^User user ^String (str pid)))
(.setUser scope ^User user)))
(set-level! [^Scope scope]
(.setLevel scope SentryLevel/ERROR))
(set-context! [^Scope scope {:keys [context] :as event}]
(let [uri (str (cf/get :public-uri) "/dbg/error-by-id/" (:id context))]
(.setContexts scope "detailed_error_uri" ^String uri))
(when-let [vers (:frontend-version event)]
(.setContexts scope "frontend_version" ^String vers))
(when-let [puri (:public-uri event)]
(.setContexts scope "public_uri" ^String (str puri)))
(when-let [uagent (:user-agent context)]
(.setContexts scope "user_agent" ^String uagent))
(when-let [tenant (:tenant event)]
(.setTag scope "tenant" ^String tenant))
(when-let [type (:error-type context)]
(.setTag scope "error_type" ^String (str type)))
(when-let [code (:error-code context)]
(.setTag scope "error_code" ^String (str code)))
)
(capture [^Scope scope {:keys [context error] :as event}]
(let [msg (str (:message error) "\n\n"
"======================================================\n"
"=================== Params ===========================\n"
"======================================================\n"
(:params context) "\n"
(when (:explain context)
(str "======================================================\n"
"=================== Explain ==========================\n"
"======================================================\n"
(:explain context) "\n"))
(when (:data context)
(str "======================================================\n"
"=================== Error Data =======================\n"
"======================================================\n"
(:data context) "\n"))
(str "======================================================\n"
"=================== Stack Trace ======================\n"
"======================================================\n"
(:trace error))
"\n")]
(set-user! scope event)
(set-level! scope)
(set-context! scope event)
(.captureMessage ^IHub shub msg)
))
]
(when @enabled
(.withScope ^IHub shub (reify ScopeCallback
(run [_ scope]
(->> event
(parse-event)
(capture scope))))))
))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Error Listener
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::receiver any?)
(s/def ::dsn ::cf/sentry-dsn)
(s/def ::trace-sample-rate ::cf/sentry-trace-sample-rate)
(s/def ::attach-stack-trace ::cf/sentry-attach-stack-trace)
(s/def ::debug ::cf/sentry-debug)
(defmethod ig/pre-init-spec ::reporter [_]
(s/keys :req-un [::wrk/executor ::db/pool ::receiver]
:opt-un [::dsn ::trace-sample-rate ::attach-stack-trace]))
(defmethod ig/init-key ::reporter
[_ {:keys [receiver dsn executor] :as cfg}]
(l/info :msg "initializing sentry reporter" :dsn dsn)
(let [opts (build-sentry-options cfg)
shub (if dsn
(Hub. ^SentryOptions opts)
(NoOpHub/getInstance))
output (a/chan (a/sliding-buffer 128)
(filter #(= (:level %) "error")))]
(receiver :sub output)
(a/go-loop []
(let [event (a/<! output)]
(if (nil? event)
(do
(l/info :msg "stoping error reporting loop")
(.close ^IHub shub))
(do
(a/<! (aa/with-thread executor (handle-event shub event)))
(recur)))))
output))
(defmethod ig/halt-key! ::reporter
[_ output]
(when output
(a/close! output)))

View file

@ -244,6 +244,9 @@
{:name "0078-mod-file-media-object-table-drop-cascade"
:fn (mg/resource "app/migrations/sql/0078-mod-file-media-object-table-drop-cascade.sql")}
{:name "0079-mod-profile-table"
:fn (mg/resource "app/migrations/sql/0079-mod-profile-table.sql")}
])

View file

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

View file

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

View file

@ -6,6 +6,7 @@
(ns app.rpc.commands.auth
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
@ -96,15 +97,19 @@
(:valid (verify-password password (:password profile))))
(validate-profile [profile]
(when-not (:is-active profile)
(ex/raise :type :validation
:code :wrong-credentials))
(when-not profile
(ex/raise :type :validation
:code :wrong-credentials))
(when-not (:is-active profile)
(ex/raise :type :validation
:code :wrong-credentials))
(when (:is-blocked profile)
(ex/raise :type :restriction
:code :profile-blocked))
(when-not (check-password profile password)
(ex/raise :type :validation
:code :wrong-credentials))
profile)]
(db/with-atomic [conn pool]
@ -184,8 +189,9 @@
;; ---- COMMAND: Prepare Register
(defn prepare-register
[{:keys [pool sprops] :as cfg} params]
(defn validate-register-attempt!
[{:keys [pool sprops]} params]
(when-not (contains? cf/flags :registration)
(if-not (contains? params :invitation-token)
(ex/raise :type :restriction
@ -208,20 +214,50 @@
:code :email-has-permanent-bounces
:hint "looks like the email has one or many bounces reported"))
(check-profile-existence! pool params)
;; Perform a basic validation of email & password
(when (= (str/lower (:email params))
(str/lower (:password params)))
(ex/raise :type :validation
:code :email-as-password
:hint "you can't use your email as password"))
:hint "you can't use your email as password")))
(let [params {:email (:email params)
:password (:password params)
:invitation-token (:invitation-token params)
:backend "penpot"
:iss :prepared-register
:exp (dt/in-future "48h")}
(def register-retry-threshold
(dt/duration "15m"))
(defn- elapsed-register-retry-threshold?
[profile]
(let [elapsed (dt/diff (:modified-at profile) (dt/now))]
(pos? (compare elapsed register-retry-threshold))))
(defn prepare-register
[{:keys [pool sprops] :as cfg} params]
(validate-register-attempt! cfg params)
(let [profile (when-let [profile (profile/retrieve-profile-data-by-email pool (:email params))]
(cond
(:is-blocked profile)
(ex/raise :type :restriction
:code :profile-blocked)
(and (not (:is-active profile))
(elapsed-register-retry-threshold? profile))
profile
:else
(ex/raise :type :validation
:code :email-already-exists
:hint "profile already exists")))
params {:email (:email params)
:password (:password params)
:invitation-token (:invitation-token params)
:backend "penpot"
:iss :prepared-register
:profile-id (:id profile)
:exp (dt/in-future {:days 7})}
params (d/without-nils params)
token (tokens/generate sprops params)]
(with-meta {:token token}
@ -240,11 +276,10 @@
;; ---- COMMAND: Register Profile
(defn create-profile
"Create the profile entry on the database with limited input filling
all the other fields with defaults."
"Create the profile entry on the database with limited set of input
attrs (all the other attrs are filled with default values)."
[conn params]
(let [id (or (:id params) (uuid/next))
props (-> (audit/extract-utm-params params)
(merge (:props params))
(merge {:viewed-tutorial? false
@ -320,62 +355,76 @@
(defn register-profile
[{:keys [conn sprops session] :as cfg} {:keys [token] :as params}]
(let [claims (tokens/verify sprops {:token token :iss :prepared-register})
params (merge params claims)]
(check-profile-existence! conn params)
(let [is-active (or (:is-active params)
(not (contains? cf/flags :email-verification))
(let [claims (tokens/verify sprops {:token token :iss :prepared-register})
params (merge params claims)
;; DEPRECATED: v1.15
(contains? cf/flags :insecure-register))
is-active (or (:is-active params)
(not (contains? cf/flags :email-verification))
profile (->> (assoc params :is-active is-active)
;; DEPRECATED: v1.15
(contains? cf/flags :insecure-register))
profile (if-let [profile-id (:profile-id claims)]
(profile/retrieve-profile conn profile-id)
(->> (assoc params :is-active is-active)
(create-profile conn)
(create-profile-relations conn)
(profile/decode-profile-row))
(profile/decode-profile-row)))
audit-fn (:audit cfg)
invitation (when-let [token (:invitation-token params)]
(tokens/verify sprops {:token token :iss :team-invitation}))]
(cond
;; If invitation token comes in params, this is because the
;; user comes from team-invitation process; in this case,
;; regenerate token and send back to the user a new invitation
;; token (and mark current session as logged). This happens
;; only if the invitation email matches with the register
;; email.
(and (some? invitation) (= (:email profile) (:member-email invitation)))
(let [claims (assoc invitation :member-id (:id profile))
token (tokens/generate sprops claims)
resp {:invitation-token token}]
(with-meta resp
{:transform-response ((:create session) (:id profile))
::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)}))
invitation (when-let [token (:invitation-token params)]
(tokens/verify sprops {:token token :iss :team-invitation}))]
;; If auth backend is different from "penpot" means user is
;; registering using third party auth mechanism; in this case
;; we need to mark this session as logged.
(not= "penpot" (:auth-backend profile))
(with-meta (profile/strip-private-attrs profile)
;; If profile is filled in claims, means it tries to register
;; again, so we proceed to update the modified-at attr
;; accordingly.
(when-let [id (:profile-id claims)]
(db/update! conn :profile {:modified-at (dt/now)} {:id id})
(audit-fn :cmd :submit
:type "fact"
:name "register-profile-retry"
:profile-id id))
(cond
;; If invitation token comes in params, this is because the
;; user comes from team-invitation process; in this case,
;; regenerate token and send back to the user a new invitation
;; token (and mark current session as logged). This happens
;; only if the invitation email matches with the register
;; email.
(and (some? invitation) (= (:email profile) (:member-email invitation)))
(let [claims (assoc invitation :member-id (:id profile))
token (tokens/generate sprops claims)
resp {:invitation-token token}]
(with-meta resp
{:transform-response ((:create session) (:id profile))
::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})
::audit/profile-id (:id profile)}))
;; If the `:enable-insecure-register` flag is set, we proceed
;; to sign in the user directly, without email verification.
(true? is-active)
(with-meta (profile/strip-private-attrs profile)
{:transform-response ((:create session) (:id profile))
::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})
;; If auth backend is different from "penpot" means user is
;; registering using third party auth mechanism; in this case
;; we need to mark this session as logged.
(not= "penpot" (:auth-backend profile))
(with-meta (profile/strip-private-attrs profile)
{:transform-response ((:create session) (:id profile))
::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})
;; In all other cases, send a verification email.
:else
(do
(send-email-verification! conn sprops profile)
(with-meta profile
{::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)}))))))
;; If the `:enable-insecure-register` flag is set, we proceed
;; to sign in the user directly, without email verification.
(true? is-active)
(with-meta (profile/strip-private-attrs profile)
{:transform-response ((:create session) (:id profile))
::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})
;; In all other cases, send a verification email.
:else
(do
(send-email-verification! conn sprops profile)
(with-meta profile
{::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})))))
(s/def ::register-profile
(s/keys :req-un [::token ::fullname]))

View file

@ -46,6 +46,11 @@
:code :wrong-credentials))
(let [profile (login-or-register cfg info)]
(when (:is-blocked profile)
(ex/raise :type :restriction
:code :profile-blocked))
(if-let [token (:invitation-token params)]
;; If invitation token comes in params, this is because the
;; user comes from team-invitation process; in this case,

View file

@ -0,0 +1,193 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.rpc.commands.verify-token
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.db :as db]
[app.loggers.audit :as audit]
[app.rpc.doc :as-alias doc]
[app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile]
[app.tokens :as tokens]
[app.tokens.spec.team-invitation :as-alias spec.team-invitation]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
(s/def ::iss keyword?)
(s/def ::exp ::us/inst)
(defmulti process-token (fn [_ _ claims] (:iss claims)))
(s/def ::verify-token
(s/keys :req-un [::token]
:opt-un [::profile-id]))
(sv/defmethod ::verify-token
{:auth false
::doc/added "1.15"}
[{:keys [pool sprops] :as cfg} {:keys [token] :as params}]
(db/with-atomic [conn pool]
(let [claims (tokens/verify sprops {:token token})
cfg (assoc cfg :conn conn)]
(process-token cfg params claims))))
(defmethod process-token :change-email
[{:keys [conn] :as cfg} _params {:keys [profile-id email] :as claims}]
(when (profile/retrieve-profile-data-by-email conn email)
(ex/raise :type :validation
:code :email-already-exists))
(db/update! conn :profile
{:email email}
{:id profile-id})
(with-meta claims
{::audit/name "update-profile-email"
::audit/props {:email email}
::audit/profile-id profile-id}))
(defmethod process-token :verify-email
[{:keys [conn session] :as cfg} _ {:keys [profile-id] :as claims}]
(let [profile (profile/retrieve-profile conn profile-id)
claims (assoc claims :profile profile)]
(when-not (:is-active profile)
(when (not= (:email profile)
(:email claims))
(ex/raise :type :validation
:code :invalid-token))
(db/update! conn :profile
{:is-active true}
{:id (:id profile)}))
(with-meta claims
{:transform-response ((:create session) profile-id)
::audit/name "verify-profile-email"
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)})))
(defmethod process-token :auth
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
(let [profile (profile/retrieve-profile conn profile-id)]
(assoc claims :profile profile)))
;; --- Team Invitation
(defn- accept-invitation
[{:keys [conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member]
(let [;; Update the role if there is an invitation
role (or (some-> invitation :role keyword) role)
params (merge
{:team-id team-id
:profile-id (:id member)}
(teams/role->params role))]
;; Do not allow blocked users accept invitations.
(when (:is-blocked member)
(ex/raise :type :restriction
:code :profile-blocked))
;; Insert the invited member to the team
(db/insert! conn :team-profile-rel params {:on-conflict-do-nothing true})
;; If profile is not yet verified, mark it as verified because
;; accepting an invitation link serves as verification.
(when-not (:is-active member)
(db/update! conn :profile
{:is-active true}
{:id (:id member)}))
;; Delete the invitation
(db/delete! conn :team-invitation
{:team-id team-id :email-to member-email})
(assoc member :is-active true)))
(s/def ::spec.team-invitation/profile-id ::us/uuid)
(s/def ::spec.team-invitation/role ::us/keyword)
(s/def ::spec.team-invitation/team-id ::us/uuid)
(s/def ::spec.team-invitation/member-email ::us/email)
(s/def ::spec.team-invitation/member-id (s/nilable ::us/uuid))
(s/def ::team-invitation-claims
(s/keys :req-un [::iss ::exp
::spec.team-invitation/profile-id
::spec.team-invitation/role
::spec.team-invitation/team-id
::spec.team-invitation/member-email]
:opt-un [::spec.team-invitation/member-id]))
(defmethod process-token :team-invitation
[{:keys [conn session] :as cfg} {:keys [profile-id token]}
{:keys [member-id team-id member-email] :as claims}]
(us/assert ::team-invitation-claims claims)
(let [invitation (db/get* conn :team-invitation
{:team-id team-id :email-to member-email})
profile (db/get* conn :profile
{:id profile-id}
{:columns [:id :email]})]
(when (nil? invitation)
(ex/raise :type :validation
:code :invalid-token
:hint "no invitation associated with the token"))
(if (some? profile)
(if (or (= member-id profile-id)
(= member-email (:email profile)))
;; if we have logged-in user and it matches the invitation we
;; proceed with accepting the invitation and joining the
;; current profile to the invited team.
(let [profile (accept-invitation cfg claims invitation profile)]
(with-meta
(assoc claims :state :created)
{::audit/name "accept-team-invitation"
::audit/props (merge
(audit/profile->props profile)
{:team-id (:team-id claims)
:role (:role claims)})
::audit/profile-id profile-id}))
(ex/raise :type :validation
:code :invalid-token
:hint "logged-in user does not matches the invitation"))
;; If we have not logged-in user, we try find the invited
;; profile by member-id or member-email props of the invitation
;; token; If profile is found, we accept the invitation and
;; leave the user logged-in.
(if-let [member (db/get* conn :profile
(if member-id
{:id member-id}
{:email member-email})
{:columns [:id :email]})]
(let [profile (accept-invitation cfg claims invitation member)]
(with-meta
(assoc claims :state :created)
{:transform-response ((:create session) (:id profile))
::audit/name "accept-team-invitation"
::audit/props (merge
(audit/profile->props profile)
{:team-id (:team-id claims)
:role (:role claims)})
::audit/profile-id member-id}))
{:invitation-token token
:iss :team-invitation
:redirect-to :auth-register
:state :pending}))))
;; --- Default
(defmethod process-token :default
[_ _ _]
(ex/raise :type :validation
:code :invalid-token))

View file

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

View file

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

View file

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

View file

@ -6,170 +6,23 @@
(ns app.rpc.mutations.verify-token
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.db :as db]
[app.loggers.audit :as audit]
[app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile]
[app.rpc.commands.verify-token :refer [process-token]]
[app.rpc.doc :as-alias doc]
[app.tokens :as tokens]
[app.tokens.spec.team-invitation :as-alias spec.team-invitation]
[app.util.services :as sv]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
(defmulti process-token (fn [_ _ claims] (:iss claims)))
[clojure.spec.alpha :as s]))
(s/def ::verify-token
(s/keys :req-un [::token]
:opt-un [::profile-id]))
(sv/defmethod ::verify-token {:auth false}
(sv/defmethod ::verify-token
{:auth false
::doc/added "1.1"
::doc/deprecated "1.15"}
[{:keys [pool sprops] :as cfg} {:keys [token] :as params}]
(db/with-atomic [conn pool]
(let [claims (tokens/verify sprops {:token token})
cfg (assoc cfg :conn conn)]
(process-token cfg params claims))))
(defmethod process-token :change-email
[{:keys [conn] :as cfg} _params {:keys [profile-id email] :as claims}]
(when (profile/retrieve-profile-data-by-email conn email)
(ex/raise :type :validation
:code :email-already-exists))
(db/update! conn :profile
{:email email}
{:id profile-id})
(with-meta claims
{::audit/name "update-profile-email"
::audit/props {:email email}
::audit/profile-id profile-id}))
(defmethod process-token :verify-email
[{:keys [conn session] :as cfg} _ {:keys [profile-id] :as claims}]
(let [profile (profile/retrieve-profile conn profile-id)
claims (assoc claims :profile profile)]
(when-not (:is-active profile)
(when (not= (:email profile)
(:email claims))
(ex/raise :type :validation
:code :invalid-token))
(db/update! conn :profile
{:is-active true}
{:id (:id profile)}))
(with-meta claims
{:transform-response ((:create session) profile-id)
::audit/name "verify-profile-email"
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)})))
(defmethod process-token :auth
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
(let [profile (profile/retrieve-profile conn profile-id)]
(assoc claims :profile profile)))
;; --- Team Invitation
(s/def ::iss keyword?)
(s/def ::exp ::us/inst)
(s/def ::spec.team-invitation/profile-id ::us/uuid)
(s/def ::spec.team-invitation/role ::us/keyword)
(s/def ::spec.team-invitation/team-id ::us/uuid)
(s/def ::spec.team-invitation/member-email ::us/email)
(s/def ::spec.team-invitation/member-id (s/nilable ::us/uuid))
(s/def ::team-invitation-claims
(s/keys :req-un [::iss ::exp
::spec.team-invitation/profile-id
::spec.team-invitation/role
::spec.team-invitation/team-id
::spec.team-invitation/member-email]
:opt-un [::spec.team-invitation/member-id]))
(defn- accept-invitation
[{:keys [conn] :as cfg} {:keys [member-id team-id role member-email] :as claims}]
(let [
member (profile/retrieve-profile conn member-id)
invitation (db/get-by-params conn :team-invitation
{:team-id team-id :email-to (str/lower member-email)}
{:check-not-found false})
;; Update the role if there is an invitation
role (or (some-> invitation :role keyword) role)
params (merge {:team-id team-id
:profile-id member-id}
(teams/role->params role))
]
;; Insert the invited member to the team
(db/insert! conn :team-profile-rel params {:on-conflict-do-nothing true})
;; If profile is not yet verified, mark it as verified because
;; accepting an invitation link serves as verification.
(when-not (:is-active member)
(db/update! conn :profile
{:is-active true}
{:id member-id}))
(assoc member :is-active true)
;; Delete the invitation
(db/delete! conn :team-invitation
{:team-id team-id :email-to (str/lower member-email)})))
(defmethod process-token :team-invitation
[cfg {:keys [profile-id token]} {:keys [member-id] :as claims}]
(us/assert ::team-invitation-claims claims)
(let [conn (:conn cfg)
team-id (:team-id claims)
member-email (:member-email claims)
invitation (db/get-by-params conn :team-invitation
{:team-id team-id :email-to (str/lower member-email)}
{:check-not-found false})]
(when (nil? invitation)
(ex/raise :type :validation
:code :invalid-token)))
(cond
;; This happens when token is filled with member-id and current
;; user is already logged in with exactly invited account.
(and (uuid? profile-id) (uuid? member-id) (= member-id profile-id))
(let [profile (accept-invitation cfg claims)]
(with-meta
(assoc claims :state :created)
{::audit/name "accept-team-invitation"
::audit/props (merge
(audit/profile->props profile)
{:team-id (:team-id claims)
:role (:role claims)})
::audit/profile-id member-id}))
;; This case means that invitation token does not match with
;; registred user, so we need to indicate to frontend to redirect
;; it to register page.
(nil? member-id)
{:invitation-token token
:iss :team-invitation
:redirect-to :auth-register
:state :pending}
;; In all other cases, just tell to fontend to redirect the user
;; to the login page.
:else
{:invitation-token token
:iss :team-invitation
:redirect-to :auth-login
:state :pending}))
;; --- Default
(defmethod process-token :default
[_ _ _]
(ex/raise :type :validation
:code :invalid-token))

View file

@ -62,9 +62,9 @@
(cmd.auth/send-email-verification! pool sprops profile)
:email-sent))
(defn update-profile
(defn update-profile!
"Update a limited set of profile attrs."
[system & {:keys [email id active? deleted?]}]
[system & {:keys [email id active? deleted? blocked?]}]
(us/verify!
:expr (some? system)
@ -74,15 +74,30 @@
:expr (or (string? email) (uuid? id))
:hint "email or id should be provided")
(let [pool (:app.db/pool system)
params (cond-> {}
(let [params (cond-> {}
(true? active?) (assoc :is-active true)
(false? active?) (assoc :is-active false)
(true? deleted?) (assoc :deleted-at (dt/now)))
(true? deleted?) (assoc :deleted-at (dt/now))
(true? blocked?) (assoc :is-blocked true)
(false? blocked?) (assoc :is-blocked false))
opts (cond-> {}
(some? email) (assoc :email (str/lower email))
(some? id) (assoc :id id))]
(some-> (db/update! pool :profile params opts)
(profile/decode-profile-row))))
(db/with-atomic [conn (:app.db/pool system)]
(some-> (db/update! conn :profile params opts)
(profile/decode-profile-row)))))
(defn mark-profile-as-blocked!
"Mark the profile blocked and removes all the http sessiones
associated with the profile-id."
[system email]
(db/with-atomic [conn (:app.db/pool system)]
(when-let [profile (db/get-by-params conn :profile
{:email (str/lower email)}
{:columns [:id :email]
:check-not-found false})]
(when-not (:is-blocked profile)
(db/update! conn :profile {:is-blocked true} {:id (:id profile)})
(db/delete! conn :http-session {:profile-id (:id profile)})
:blocked))))

View file

@ -69,8 +69,7 @@
(defmethod delete-objects "team_font_variant"
[{:keys [conn min-age storage table] :as cfg}]
(let [sql (str/fmt sql:delete-objects
{:table table :limit 50})
(let [sql (str/fmt sql:delete-objects {:table table :limit 50})
fonts (db/exec! conn [sql min-age])
storage (media/configure-assets-storage storage conn)]
(doseq [{:keys [id] :as font} fonts]
@ -85,10 +84,9 @@
(defmethod delete-objects "team"
[{:keys [conn min-age storage table] :as cfg}]
(let [sql (str/fmt sql:delete-objects
{:table table :limit 50})
(let [sql (str/fmt sql:delete-objects {:table table :limit 50})
teams (db/exec! conn [sql min-age])
storage (assoc storage :conn conn)]
storage (media/configure-assets-storage storage conn)]
(doseq [{:keys [id] :as team} teams]
(l/debug :hint "permanently delete object" :table table :id id)
@ -103,32 +101,17 @@
where deleted_at is not null
and deleted_at < now() - ?::interval
order by deleted_at
limit %(limit)s
limit ?
for update")
(def sql:mark-owned-teams-deleted
"with owned as (
select tpr.team_id as id
from team_profile_rel as tpr
where tpr.is_owner is true
and tpr.profile_id = ?
)
update team set deleted_at = now() - ?::interval
where id in (select id from owned)")
(defmethod delete-objects "profile"
[{:keys [conn min-age storage table] :as cfg}]
(let [sql (str/fmt sql:retrieve-deleted-profiles {:limit 50})
profiles (db/exec! conn [sql min-age])
storage (assoc storage :conn conn)]
(let [profiles (db/exec! conn [sql:retrieve-deleted-profiles min-age 50])
storage (media/configure-assets-storage storage conn)]
(doseq [{:keys [id] :as profile} profiles]
(l/debug :hint "permanently delete object" :table table :id id)
;; Mark the owned teams as deleted; this enables them to be processed
;; in the same transaction in the "team" table step.
(db/exec-one! conn [sql:mark-owned-teams-deleted id min-age])
;; Mark as deleted the storage object related with the photo-id
;; field.
(some->> (:photo-id profile) (sto/touch-object! storage) deref)
@ -164,22 +147,23 @@
(defmethod ig/init-key ::handler
[_ {:keys [pool] :as cfg}]
(fn [params]
;; Checking first on task argument allows properly testing it.
(let [min-age (or (:min-age params) (:min-age cfg))]
(db/with-atomic [conn pool]
(let [cfg (-> cfg
(assoc :min-age (db/interval min-age))
(assoc :conn conn))]
(loop [tables (seq target-tables)
total 0]
(if-let [table (first tables)]
(recur (rest tables)
(+ total (process-table (assoc cfg :table table))))
(do
(l/info :hint "task finished" :min-age (dt/format-duration min-age) :total total)
(db/with-atomic [conn pool]
(let [min-age (or (:min-age params) (:min-age cfg))
cfg (-> cfg
(assoc :min-age (db/interval min-age))
(assoc :conn conn))]
(loop [tables (seq target-tables)
total 0]
(if-let [table (first tables)]
(recur (rest tables)
(+ total (process-table (assoc cfg :table table))))
(do
(l/info :hint "objects gc finished succesfully"
:min-age (dt/format-duration min-age)
:total total)
(when (:rollback? params)
(db/rollback! conn))
(when (:rollback? params)
(db/rollback! conn))
{:processed total}))))))))
{:processed total})))))))

View file

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

View file

@ -11,6 +11,20 @@
[app.common.data :as d]
[cuerdas.core :as str]))
(defrecord WrappedValue [obj]
clojure.lang.IDeref
(deref [_] obj))
(defn wrap
([]
(WrappedValue. nil))
([o]
(WrappedValue. o)))
(defn wrapped?
[o]
(instance? WrappedValue o))
(defmacro defmethod
[sname & body]
(let [[docs body] (if (string? (first body))

View file

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

View file

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

View file

@ -119,15 +119,14 @@
))
(t/deftest profile-deletion-simple
(let [task (:app.tasks.objects-gc/handler th/*system*)
prof (th/create-profile* 1)
(let [prof (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id prof)
:project-id (:default-project-id prof)
:is-shared false})]
;; profile is not deleted because it does not meet all
;; conditions to be deleted.
(let [result (task {:min-age (dt/duration 0)})]
(let [result (th/run-task! :objects-gc {:min-age (dt/duration 0)})]
(t/is (= 0 (:processed result))))
;; Request profile to be deleted
@ -146,7 +145,7 @@
(t/is (= 1 (count (:result out)))))
;; execute permanent deletion task
(let [result (task {:min-age (dt/duration "-1m")})]
(let [result (th/run-task! :objects-gc {:min-age (dt/duration "-1m")})]
(t/is (= 1 (:processed result))))
;; query profile after delete
@ -166,7 +165,7 @@
(t/testing "not allowed email domain"
(t/is (false? (cauth/email-domain-in-whitelist? whitelist "username@somedomain.com"))))))
(t/deftest prepare-register-and-register-profile
(t/deftest prepare-register-and-register-profile-1
(let [data {::th/type :prepare-register-profile
:email "user@example.com"
:password "foobar"}
@ -195,6 +194,100 @@
(t/is (nil? error))))
))
(t/deftest prepare-register-and-register-profile-1
(let [data {::th/type :prepare-register-profile
:email "user@example.com"
:password "foobar"}
out (th/mutation! data)
token (get-in out [:result :token])]
(t/is (string? token))
;; try register without token
(let [data {::th/type :register-profile
:fullname "foobar"
:accept-terms-and-privacy true}
out (th/mutation! data)]
(let [error (:error out)]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :spec-validation))))
;; try correct register
(let [data {::th/type :register-profile
:token token
:fullname "foobar"
:accept-terms-and-privacy true
:accept-newsletter-subscription true}]
(let [{:keys [result error]} (th/mutation! data)]
(t/is (nil? error))))
))
(t/deftest prepare-register-and-register-profile-2
(with-redefs [app.rpc.commands.auth/register-retry-threshold (dt/duration 500)]
(with-mocks [mock {:target 'app.emails/send! :return nil}]
(let [current-token (atom nil)]
;; PREPARE REGISTER
(let [data {::th/type :prepare-register-profile
:email "hello@example.com"
:password "foobar"}
out (th/command! data)
token (get-in out [:result :token])]
(t/is (string? token))
(reset! current-token token))
;; DO REGISTRATION: try correct register attempt 1
(let [data {::th/type :register-profile
:token @current-token
:fullname "foobar"
:accept-terms-and-privacy true
:accept-newsletter-subscription true}
out (th/command! data)]
(t/is (nil? (:error out)))
(t/is (= 1 (:call-count @mock))))
(th/reset-mock! mock)
;; PREPARE REGISTER without waiting for threshold
(let [data {::th/type :prepare-register-profile
:email "hello@example.com"
:password "foobar"}
out (th/command! data)]
(t/is (not (th/success? out)))
(t/is (= :validation (-> out :error th/ex-type)))
(t/is (= :email-already-exists (-> out :error th/ex-code))))
(th/sleep {:millis 500})
(th/reset-mock! mock)
;; PREPARE REGISTER waiting the threshold
(let [data {::th/type :prepare-register-profile
:email "hello@example.com"
:password "foobar"}
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 0 (:call-count @mock)))
(let [result (:result out)]
(t/is (contains? result :token))
(reset! current-token (:token result))))
;; DO REGISTRATION: try correct register attempt 1
(let [data {::th/type :register-profile
:token @current-token
:fullname "foobar"
:accept-terms-and-privacy true
:accept-newsletter-subscription true}
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock))))
))
))
(t/deftest prepare-and-register-with-invitation-and-disabled-registration-1
(with-redefs [app.config/flags [:disable-registration]]
(let [sprops (:app.setup/props th/*system*)
@ -239,34 +332,39 @@
:invitation-token itoken
:email "user@example.com"
:password "foobar"}
{:keys [result error] :as out} (th/mutation! data)]
(t/is (th/ex-info? error))
(t/is (= :restriction (th/ex-type error)))
(t/is (= :email-does-not-match-invitation (th/ex-code error))))))
out (th/command! data)]
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :restriction (:type edata)))
(t/is (= :email-does-not-match-invitation (:code edata))))
)))
(t/deftest prepare-register-with-registration-disabled
(th/with-mocks {#'app.config/flags nil}
(with-redefs [app.config/flags #{}]
(let [data {::th/type :prepare-register-profile
:email "user@example.com"
:password "foobar"}]
(let [{:keys [result error] :as out} (th/mutation! data)]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :restriction))
(t/is (th/ex-of-code? error :registration-disabled))))))
:password "foobar"}
out (th/command! data)]
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :restriction (:type edata)))
(t/is (= :registration-disabled (:code edata)))))))
(t/deftest prepare-register-with-existing-user
(let [profile (th/create-profile* 1)
data {::th/type :prepare-register-profile
:email (:email profile)
:password "foobar"}]
(let [{:keys [result error] :as out} (th/mutation! data)]
;; (th/print-result! out)
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :email-already-exists)))))
:password "foobar"}
out (th/command! data)]
(t/deftest test-register-profile-with-bounced-email
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :validation (:type edata)))
(t/is (= :email-already-exists (:code edata))))))
(t/deftest register-profile-with-bounced-email
(let [pool (:app.db/pool th/*system*)
data {::th/type :prepare-register-profile
:email "user@example.com"
@ -274,34 +372,38 @@
(th/create-global-complaint-for pool {:type :bounce :email "user@example.com"})
(let [{:keys [result error] :as out} (th/mutation! data)]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :email-has-permanent-bounces)))))
(let [out (th/command! data)]
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :validation (:type edata)))
(t/is (= :email-has-permanent-bounces (:code edata)))))))
(t/deftest test-register-profile-with-complained-email
(t/deftest register-profile-with-complained-email
(let [pool (:app.db/pool th/*system*)
data {::th/type :prepare-register-profile
:email "user@example.com"
:password "foobar"}]
(th/create-global-complaint-for pool {:type :complaint :email "user@example.com"})
(let [{:keys [result error] :as out} (th/mutation! data)]
(t/is (nil? error))
(t/is (string? (:token result))))))
(t/deftest test-register-profile-with-email-as-password
(let [data {::th/type :prepare-register-profile
:email "user@example.com"
:password "USER@example.com"}]
(let [out (th/command! data)]
(t/is (th/success? out))
(let [result (:result out)]
(t/is (contains? result :token))))))
(let [{:keys [result error] :as out} (th/mutation! data)]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :email-as-password)))))
(t/deftest register-profile-with-email-as-password
(let [data {::th/type :prepare-register-profile
:email "user@example.com"
:password "USER@example.com"}
out (th/command! data)]
(t/deftest test-email-change-request
(with-mocks [email-send-mock {:target 'app.emails/send! :return nil}]
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :validation (:type edata)))
(t/is (= :email-as-password (:code edata))))))
(t/deftest email-change-request
(with-mocks [mock {:target 'app.emails/send! :return nil}]
(let [profile (th/create-profile* 1)
pool (:app.db/pool th/*system*)
data {::th/type :request-email-change
@ -312,7 +414,7 @@
(let [out (th/mutation! data)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(let [mock (deref email-send-mock)]
(let [mock @mock]
(t/is (= 1 (:call-count mock)))
(t/is (true? (:called? mock)))))
@ -321,7 +423,7 @@
(let [out (th/mutation! data)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (= 2 (:call-count (deref email-send-mock)))))
(t/is (= 2 (:call-count @mock))))
;; with bounces
(th/create-global-complaint-for pool {:type :bounce :email (:email data)})
@ -331,28 +433,26 @@
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :email-has-permanent-bounces))
(t/is (= 2 (:call-count (deref email-send-mock))))))))
(t/is (= 2 (:call-count @mock)))))))
(t/deftest test-email-change-request-without-smtp
(with-mocks [email-send-mock {:target 'app.emails/send! :return nil}]
(t/deftest email-change-request-without-smtp
(with-mocks [mock {:target 'app.emails/send! :return nil}]
(with-redefs [app.config/flags #{}]
(let [profile (th/create-profile* 1)
pool (:app.db/pool th/*system*)
data {::th/type :request-email-change
:profile-id (:id profile)
:email "user1@example.com"}]
:email "user1@example.com"}
out (th/mutation! data)]
(let [out (th/mutation! data)
res (:result out)]
;; (th/print-result! out)
(t/is (= {:changed true} res))
(let [mock (deref email-send-mock)]
(t/is (false? (:called? mock)))))))))
;; (th/print-result! out)
(t/is (false? (:called? @mock)))
(let [res (:result out)]
(t/is (= {:changed true} res)))))))
(t/deftest test-request-profile-recovery
(t/deftest request-profile-recovery
(with-mocks [mock {:target 'app.emails/send! :return nil}]
(let [profile1 (th/create-profile* 1)
profile2 (th/create-profile* 2 {:is-active true})
@ -363,13 +463,13 @@
(let [data (assoc data :email "foo@bar.com")
out (th/mutation! data)]
(t/is (nil? (:result out)))
(t/is (= 0 (:call-count (deref mock)))))
(t/is (= 0 (:call-count @mock))))
;; with valid email inactive user
(let [data (assoc data :email (:email profile1))
out (th/mutation! data)
error (:error out)]
(t/is (= 0 (:call-count (deref mock))))
(t/is (= 0 (:call-count @mock)))
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :profile-not-verified)))
@ -379,7 +479,7 @@
out (th/mutation! data)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (= 1 (:call-count (deref mock)))))
(t/is (= 1 (:call-count @mock))))
;; with valid email and active user with global complaints
(th/create-global-complaint-for pool {:type :complaint :email (:email profile2)})
@ -387,7 +487,7 @@
out (th/mutation! data)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (= 2 (:call-count (deref mock)))))
(t/is (= 2 (:call-count @mock))))
;; with valid email and active user with global bounce
(th/create-global-complaint-for pool {:type :bounce :email (:email profile2)})
@ -395,7 +495,7 @@
out (th/mutation! data)
error (:error out)]
;; (th/print-result! out)
(t/is (= 2 (:call-count (deref mock))))
(t/is (= 2 (:call-count @mock)))
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :email-has-permanent-bounces)))

View file

@ -11,6 +11,7 @@
[app.http :as http]
[app.storage :as sto]
[app.test-helpers :as th]
[app.tokens :as tokens]
[app.util.time :as dt]
[clojure.test :as t]
[datoteka.core :as fs]
@ -19,7 +20,7 @@
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
(t/deftest test-invite-team-member
(t/deftest invite-team-member
(with-mocks [mock {:target 'app.emails/send! :return nil}]
(let [profile1 (th/create-profile* 1 {:is-active true})
profile2 (th/create-profile* 2 {:is-active true})
@ -34,17 +35,16 @@
:profile-id (:id profile1)}]
;; invite external user without complaints
(let [data (assoc data :email "foo@bar.com")
out (th/mutation! data)
;;retrieve the value from the database and check its content
(let [data (assoc data :email "foo@bar.com")
out (th/mutation! data)
;; retrieve the value from the database and check its content
invitation (db/exec-one!
th/*pool*
["select count(*) as num from team_invitation where team_id = ? and email_to = ?"
(:team-id data) "foo@bar.com"])]
th/*pool*
["select count(*) as num from team_invitation where team_id = ? and email_to = ?"
(:team-id data) "foo@bar.com"])]
;; (th/print-result! out)
(t/is (= {} (:result out)))
(t/is (th/success? out))
(t/is (= 1 (:call-count (deref mock))))
(t/is (= 1 (:num invitation))))
@ -52,7 +52,7 @@
(th/reset-mock! mock)
(let [data (assoc data :email (:email profile2))
out (th/mutation! data)]
(t/is (= {} (:result out)))
(t/is (th/success? out))
(t/is (= 1 (:call-count (deref mock)))))
;; invite user with complaint
@ -60,35 +60,183 @@
(th/reset-mock! mock)
(let [data (assoc data :email "foo@bar.com")
out (th/mutation! data)]
(t/is (= {} (:result out)))
(t/is (th/success? out))
(t/is (= 1 (:call-count (deref mock)))))
;; invite user with bounce
(th/reset-mock! mock)
(th/create-global-complaint-for pool {:type :bounce :email "foo@bar.com"})
(let [data (assoc data :email "foo@bar.com")
out (th/mutation! data)
error (:error out)]
out (th/mutation! data)]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :email-has-permanent-bounces))
(t/is (= 0 (:call-count (deref mock)))))
(t/is (not (th/success? out)))
(t/is (= 0 (:call-count @mock)))
(let [edata (-> out :error ex-data)]
(t/is (= :validation (:type edata)))
(t/is (= :email-has-permanent-bounces (:code edata)))))
;; invite internal user that is muted
(th/reset-mock! mock)
(let [data (assoc data :email (:email profile3))
out (th/mutation! data)
error (:error out)]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :member-is-muted))
(t/is (= 0 (:call-count (deref mock)))))
(let [data (assoc data :email (:email profile3))
out (th/mutation! data)]
(t/is (not (th/success? out)))
(t/is (= 0 (:call-count @mock)))
(let [edata (-> out :error ex-data)]
(t/is (= :validation (:type edata)))
(t/is (= :member-is-muted (:code edata)))))
)))
(t/deftest invitation-tokens
(with-mocks [mock {:target 'app.emails/send! :return nil}]
(let [profile1 (th/create-profile* 1 {:is-active true})
profile2 (th/create-profile* 2 {:is-active true})
team (th/create-team* 1 {:profile-id (:id profile1)})
sprops (:app.setup/props th/*system*)
pool (:app.db/pool th/*system*)]
;; Try to invite a not existing user
(let [data {::th/type :invite-team-member
:email "notexisting@example.com"
:team-id (:id team)
:role :editor
:profile-id (:id profile1)}
out (th/mutation! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock)))
(t/is (= 1 (-> out :result count)))
(let [token (-> out :result first)
claims (tokens/decode sprops token)]
(t/is (= :team-invitation (:iss claims)))
(t/is (= (:id profile1) (:profile-id claims)))
(t/is (= :editor (:role claims)))
(t/is (= (:id team) (:team-id claims)))
(t/is (= (:email data) (:member-email claims)))
(t/is (nil? (:member-id claims)))))
(th/reset-mock! mock)
;; Try to invite existing user
(let [data {::th/type :invite-team-member
:email (:email profile2)
:team-id (:id team)
:role :editor
:profile-id (:id profile1)}
out (th/mutation! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock)))
(t/is (= 1 (-> out :result count)))
(let [token (-> out :result first)
claims (tokens/decode sprops token)]
(t/is (= :team-invitation (:iss claims)))
(t/is (= (:id profile1) (:profile-id claims)))
(t/is (= :editor (:role claims)))
(t/is (= (:id team) (:team-id claims)))
(t/is (= (:email data) (:member-email claims)))
(t/is (= (:id profile2) (:member-id claims)))))
)))
(t/deftest accept-invitation-tokens
(let [profile1 (th/create-profile* 1 {:is-active true})
profile2 (th/create-profile* 2 {:is-active true})
team (th/create-team* 1 {:profile-id (:id profile1)})
sprops (:app.setup/props th/*system*)
pool (:app.db/pool th/*system*)]
(let [token (tokens/generate sprops
{:iss :team-invitation
:exp (dt/in-future "1h")
:profile-id (:id profile1)
:role :editor
:team-id (:id team)
:member-email (:email profile2)
:member-id (:id profile2)})]
;; --- Verify token as anonymous user
(db/insert! pool :team-invitation
{:team-id (:id team)
:email-to (:email profile2)
:role "editor"
:valid-until (dt/in-future "48h")})
(let [data {::th/type :verify-token :token token}
out (th/mutation! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [result (:result out)]
(t/is (= :created (:state result)))
(t/is (= (:email profile2) (:member-email result)))
(t/is (= (:id profile2) (:member-id result))))
(let [rows (db/query pool :team-profile-rel {:team-id (:id team)})]
(t/is (= 2 (count rows)))))
;; Clean members
(db/delete! pool :team-profile-rel
{:team-id (:id team)
:profile-id (:id profile2)})
;; --- Verify token as logged-in user
(db/insert! pool :team-invitation
{:team-id (:id team)
:email-to (:email profile2)
:role "editor"
:valid-until (dt/in-future "48h")})
(let [data {::th/type :verify-token :token token :profile-id (:id profile2)}
out (th/mutation! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [result (:result out)]
(t/is (= :created (:state result)))
(t/is (= (:email profile2) (:member-email result)))
(t/is (= (:id profile2) (:member-id result))))
(let [rows (db/query pool :team-profile-rel {:team-id (:id team)})]
(t/is (= 2 (count rows)))))
;; --- Verify token as logged-in wrong user
(db/insert! pool :team-invitation
{:team-id (:id team)
:email-to (:email profile2)
:role "editor"
:valid-until (dt/in-future "48h")})
(let [data {::th/type :verify-token :token token :profile-id (:id profile1)}
out (th/mutation! data)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :validation (:type edata)))
(t/is (= :invalid-token (:code edata)))))
)))
(t/deftest invite-team-member-with-email-verification-disabled
(with-mocks [mock {:target 'app.emails/send! :return nil}]
(let [profile1 (th/create-profile* 1 {:is-active true})
@ -108,20 +256,17 @@
(th/reset-mock! mock)
(let [data (assoc data :email (:email profile2))
out (th/mutation! data)]
(t/is (= {} (:result out)))
(t/is (th/success? out))
(t/is (= 0 (:call-count (deref mock)))))
(let [members (db/query pool :team-profile-rel
{:team-id (:id team)
:profile-id (:id profile2)})]
(t/is (= 1 (count members)))
(t/is (true? (-> members first :can-edit))))))))
(t/deftest test-deletion
(let [task (:app.tasks.objects-gc/handler th/*system*)
profile1 (th/create-profile* 1 {:is-active true})
(t/deftest team-deletion
(let [profile1 (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id profile1)})
pool (:app.db/pool th/*system*)
data {::th/type :delete-team
@ -130,7 +275,7 @@
;; team is not deleted because it does not meet all
;; conditions to be deleted.
(let [result (task {:min-age (dt/duration 0)})]
(let [result (th/run-task! :objects-gc {:min-age (dt/duration 0)})]
(t/is (= 0 (:processed result))))
;; query the list of teams
@ -138,7 +283,7 @@
:profile-id (:id profile1)}
out (th/query! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (th/success? out))
(let [result (:result out)]
(t/is (= 2 (count result)))
(t/is (= (:id team) (get-in result [1 :id])))
@ -149,21 +294,20 @@
:id (:id team)
:profile-id (:id profile1)}
out (th/mutation! params)]
;; (th/print-result! out)
(t/is (nil? (:error out))))
(t/is (th/success? out)))
;; query the list of teams after soft deletion
(let [data {::th/type :teams
:profile-id (:id profile1)}
out (th/query! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (th/success? out))
(let [result (:result out)]
(t/is (= 1 (count result)))
(t/is (= (:default-team-id profile1) (get-in result [0 :id])))))
;; run permanent deletion (should be noop)
(let [result (task {:min-age (dt/duration {:minutes 1})})]
(let [result (th/run-task! :objects-gc {:min-age (dt/duration {:minutes 1})})]
(t/is (= 0 (:processed result))))
;; query the list of projects after hard deletion
@ -172,13 +316,12 @@
:profile-id (:id profile1)}
out (th/query! data)]
;; (th/print-result! out)
(let [error (:error out)
error-data (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found))))
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :not-found (:type edata)))))
;; run permanent deletion
(let [result (task {:min-age (dt/duration 0)})]
(let [result (th/run-task! :objects-gc {:min-age (dt/duration 0)})]
(t/is (= 1 (:processed result))))
;; query the list of projects of a after hard deletion
@ -187,31 +330,27 @@
:profile-id (:id profile1)}
out (th/query! data)]
;; (th/print-result! out)
(let [error (:error out)
error-data (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found))))
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :not-found (:type edata)))))
))
(t/deftest query-team-invitations
(let [prof (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id prof)})
(let [prof (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id prof)})
data {::th/type :team-invitations
:profile-id (:id prof)
:team-id (:id team)}]
;;insert an entry on the database with an enabled invitation
;; insert an entry on the database with an enabled invitation
(db/insert! th/*pool* :team-invitation
{:team-id (:team-id data)
:email-to "test1@mail.com"
:role "editor"
:valid-until (dt/in-future "48h")})
{:team-id (:team-id data)
:email-to "test1@mail.com"
:role "editor"
:valid-until (dt/in-future "48h")})
;;insert an entry on the database with an expired invitation
;; insert an entry on the database with an expired invitation
(db/insert! th/*pool* :team-invitation
{:team-id (:team-id data)
:email-to "test2@mail.com"
@ -219,27 +358,26 @@
:valid-until (dt/in-past "48h")})
(let [out (th/query! data)]
(t/is (nil? (:error out)))
(t/is (th/success? out))
(let [result (:result out)
one (first result)
two (second result)]
one (first result)
two (second result)]
(t/is (= 2 (count result)))
(t/is (= "test1@mail.com" (:email one)))
(t/is (= "test2@mail.com" (:email two)))
(t/is (false? (:expired one)))
(t/is (true? (:expired two)))))))
(t/deftest update-team-invitation-role
(let [prof (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id prof)})
(let [prof (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id prof)})
data {::th/type :update-team-invitation-role
:profile-id (:id prof)
:team-id (:id team)
:email "TEST1@mail.com"
:role :admin}]
;;insert an entry on the database with an invitation
;; insert an entry on the database with an invitation
(db/insert! th/*pool* :team-invitation
{:team-id (:team-id data)
:email-to "test1@mail.com"
@ -247,24 +385,22 @@
:valid-until (dt/in-future "48h")})
(let [out (th/mutation! data)
;;retrieve the value from the database and check its content
result (db/get-by-params th/*pool* :team-invitation
{:team-id (:team-id data) :email-to "test1@mail.com"}
{:check-not-found false})]
(t/is (nil? (:error out)))
;; retrieve the value from the database and check its content
res (db/get* th/*pool* :team-invitation
{:team-id (:team-id data) :email-to "test1@mail.com"})]
(t/is (th/success? out))
(t/is (nil? (:result out)))
(t/is (= "admin" (:role result))))))
(t/is (= "admin" (:role res))))))
(t/deftest delete-team-invitation
(let [prof (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id prof)})
(let [prof (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id prof)})
data {::th/type :delete-team-invitation
:profile-id (:id prof)
:team-id (:id team)
:email "TEST1@mail.com"}]
;;insert an entry on the database with an invitation
;; insert an entry on the database with an invitation
(db/insert! th/*pool* :team-invitation
{:team-id (:team-id data)
:email-to "test1@mail.com"
@ -272,10 +408,10 @@
:valid-until (dt/in-future "48h")})
(let [out (th/mutation! data)
;;retrieve the value from the database and check its content
result (db/get-by-params th/*pool* :team-invitation
{:team-id (:team-id data) :email-to "test1@mail.com"}
{:check-not-found false})]
(t/is (nil? (:error out)))
;; retrieve the value from the database and check its content
res (db/get* th/*pool* :team-invitation
{:team-id (:team-id data) :email-to "test1@mail.com"})]
(t/is (th/success? out))
(t/is (nil? (:result out)))
(t/is (nil? result)))))
(t/is (nil? res)))))

View file

@ -23,9 +23,11 @@
[app.rpc.mutations.projects :as projects]
[app.rpc.mutations.teams :as teams]
[app.util.blob :as blob]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[clojure.test :as t]
[cuerdas.core :as str]
[datoteka.core :as fs]
[environ.core :refer [env]]
@ -277,8 +279,10 @@
(defmacro try-on!
[expr]
`(try
{:error nil
:result (deref ~expr)}
(let [result# (deref ~expr)
result# (cond-> result# (sv/wrapped? result#) deref)]
{:error nil
:result result#})
(catch Exception e#
{:error (handle-error e#)
:result nil})))
@ -299,6 +303,14 @@
(let [method-fn (get-in *system* [:app.rpc/methods :queries type])]
(try-on! (method-fn (dissoc data ::type)))))
(defn run-task!
([name]
(run-task! name {}))
([name params]
(let [tasks (:app.worker/registry *system*)]
(let [task-fn (get tasks name)]
(task-fn params)))))
;; --- UTILS
(defn print-error!
@ -358,6 +370,10 @@
(let [data (ex-data e)]
(= code (:code data))))
(defn success?
[{:keys [result error]}]
(nil? error))
(defn tempfile
[source]
(let [rsc (io/resource source)
@ -366,29 +382,6 @@
(io/file tmp))
tmp))
(defn sleep
[ms]
(Thread/sleep ms))
(defn mock-config-get-with
"Helper for mock app.config/get"
[data]
(fn
([key]
(get data key (get cf/config key)))
([key default]
(get data key (get cf/config key default)))))
(defmacro with-mocks
[rebinds & body]
`(with-redefs-fn ~rebinds
(fn [] ~@body)))
(defn reset-mock!
[m]
(reset! m @(mk/make-mock {})))
(defn pause
[]
(let [^java.io.Console cnsl (System/console)]
@ -408,3 +401,18 @@
[& params]
(apply db/query *pool* params))
(defn sleep
[ms-or-duration]
(Thread/sleep (inst-ms (dt/duration ms-or-duration))))
(defn config-get-mock
[data]
(fn
([key]
(get data key (get cf/config key)))
([key default]
(get data key (get cf/config key default)))))
(defn reset-mock!
[m]
(reset! m @(mk/make-mock {})))

View file

@ -3,22 +3,22 @@
org.clojure/data.json {:mvn/version "2.4.0"}
org.clojure/tools.cli {:mvn/version "1.0.206"}
metosin/jsonista {:mvn/version "0.3.6"}
org.clojure/clojurescript {:mvn/version "1.11.57"}
org.clojure/clojurescript {:mvn/version "1.11.60"}
;; Logging
org.apache.logging.log4j/log4j-api {:mvn/version "2.17.2"}
org.apache.logging.log4j/log4j-core {:mvn/version "2.17.2"}
org.apache.logging.log4j/log4j-web {:mvn/version "2.17.2"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.17.2"}
org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.17.2"}
org.apache.logging.log4j/log4j-api {:mvn/version "2.19.0"}
org.apache.logging.log4j/log4j-core {:mvn/version "2.19.0"}
org.apache.logging.log4j/log4j-web {:mvn/version "2.19.0"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.19.0"}
org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.18.0"}
org.slf4j/slf4j-api {:mvn/version "2.0.0-alpha1"}
selmer/selmer {:mvn/version "1.12.51"}
selmer/selmer {:mvn/version "1.12.55"}
criterium/criterium {:mvn/version "0.4.6"}
expound/expound {:mvn/version "0.9.0"}
com.cognitect/transit-clj {:mvn/version "1.0.329"}
com.cognitect/transit-cljs {:mvn/version "0.8.269"}
com.cognitect/transit-cljs {:mvn/version "0.8.280"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/promesa {:mvn/version "8.0.450"}
@ -42,21 +42,22 @@
{:extra-deps
{org.clojure/tools.namespace {:mvn/version "RELEASE"}
org.clojure/test.check {:mvn/version "RELEASE"}
thheller/shadow-cljs {:mvn/version "2.19.8"}
thheller/shadow-cljs {:mvn/version "2.20.2"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "RELEASE"}
mockery/mockery {:mvn/version "RELEASE"}}
:extra-paths ["test" "dev"]}
:build
{:extra-deps {io.github.clojure/tools.build {:git/tag "v0.8.1" :git/sha "7d40500"}}
{:extra-deps {io.github.clojure/tools.build {:git/tag "v0.8.3" :git/sha "0d20256"}}
:ns-default build}
:test
{:extra-paths ["test"]
:extra-deps
{io.github.cognitect-labs/test-runner
{:git/tag "v0.5.0" :git/sha "b3fd0d2"}}
{:git/tag "v0.5.1" :git/sha "dfb30dd"}}
:main-opts ["-m" "cognitect.test-runner"]
:exec-fn cognitect.test-runner.api/test}
:shadow-cljs

View file

@ -107,12 +107,12 @@ RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
ESUM='37ceaf232a85cce46bcccfd71839854e8b14bf3160e7ef72a676b9cae45ee8af'; \
BINARY_URL='https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_aarch64_linux_hotspot_18.0.1_10.tar.gz'; \
ESUM='c640fc5e5710dba3f92099a791be50fab54f91cf2c3838cb536ded27ecc562a6'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu19.28.81-ca-jdk19.0.0-linux_aarch64.tar.gz'; \
;; \
amd64|x86_64) \
ESUM='16b1d9d75f22c157af04a1fd9c664324c7f4b5163c022b382a2f2e8897c1b0a2'; \
BINARY_URL='https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_linux_hotspot_18.0.1_10.tar.gz'; \
ESUM='6813da339124261092daab369a1c60dea5f27f4ba9608a16517191d30511a087'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu19.28.81-ca-jdk19.0.0-linux_x64.tar.gz'; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \

View file

@ -8,8 +8,7 @@
PENPOT_PUBLIC_URI=http://localhost:9001
## Feature flags.
PENPOT_FLAGS="enable-registration enable-login disable-email-verification"
PENPOT_FLAGS=enable-registration enable-login disable-email-verification
## Temporal workaround because of bad builtin default

View file

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

View file

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

View file

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

View file

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

View file

@ -90,12 +90,13 @@
(fn [{:keys [:response/body :response/status] :as exchange}]
(cond
(map? body)
(let [data (t/encode-str body {:type :json-verbose})]
(let [data (t/encode-str body {:type :json-verbose})
size (js/Buffer.byteLength data "utf-8")]
(-> exchange
(assoc :response/body data)
(assoc :response/status 200)
(update :response/headers assoc "content-type" "application/transit+json")
(update :response/headers assoc "content-length" (count data))))
(update :response/headers assoc "content-length" size)))
(and (nil? body)
(= 200 status))

View file

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

View file

@ -10,9 +10,13 @@
funcool/beicon {:mvn/version "2021.07.05-1"}
funcool/okulary {:mvn/version "2022.04.11-16"}
funcool/potok {:mvn/version "2022.04.28-67"}
funcool/rumext {:mvn/version "2022.04.19-148"}
funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/rumext
{:git/tag "v2.0"
:git/sha "fc617a8"
:git/url "https://github.com/funcool/rumext.git"}
instaparse/instaparse {:mvn/version "1.4.12"}
garden/garden {:git/url "https://github.com/noprompt/garden"
:git/sha "05590ecb5f6fa670856f3d1ab400aa4961047480"}
@ -32,7 +36,7 @@
:dev
{:extra-paths ["dev"]
:extra-deps
{thheller/shadow-cljs {:mvn/version "2.19.9"}
{thheller/shadow-cljs {:mvn/version "2.20.2"}
org.clojure/tools.namespace {:mvn/version "RELEASE"}
cider/cider-nrepl {:mvn/version "0.28.4"}}}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,60 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.sentry
"Sentry integration."
(:require
["@sentry/browser" :as sentry]
[app.common.exceptions :as ex]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.refs :as refs]))
(defn- setup-profile!
[profile]
(if (or (= uuid/zero (:id profile))
(nil? profile))
(sentry/setUser nil)
(sentry/setUser #js {:id (str (:id profile))})))
(defn init!
[]
(setup-profile! @refs/profile)
(when cf/sentry-dsn
(sentry/init
#js {:dsn cf/sentry-dsn
:autoSessionTracking false
:attachStacktrace false
:release (str "frontend@" (:base @cf/version))
:maxBreadcrumbs 20
:beforeBreadcrumb (fn [breadcrumb _hint]
(let [category (.-category ^js breadcrumb)]
(if (= category "navigate")
breadcrumb
nil)))
:tracesSampleRate 1.0})
(add-watch refs/profile ::profile
(fn [_ _ _ profile]
(setup-profile! profile)))
(add-watch refs/route ::route
(fn [_ _ _ route]
(sentry/addBreadcrumb
#js {:category "navigate",
:message (str "path: " (:path route))
:level (.-Info ^js sentry/Severity)})))))
(defn capture-exception
[err]
(when cf/sentry-dsn
(when (ex/ex-info? err)
(sentry/setContext "ex-data", (clj->js (ex-data err))))
(sentry/captureException err))
err)

View file

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

View file

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

View file

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

View file

@ -20,7 +20,7 @@
[app.util.router :as rt]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]))
[rumext.v2 :as mf]))
(def show-alt-login-buttons?
(some (partial contains? @cf/flags)
@ -83,14 +83,24 @@
form (fm/use-form :spec ::login-form :initial initial)
on-error
(fn [_]
(reset! error (tr "errors.wrong-credentials")))
(fn [cause]
(cond
(and (= :restriction (:type cause))
(= :profile-blocked (:code cause)))
(reset! error (tr "errors.profile-blocked"))
(and (= :validation (:type cause))
(= :wrong-credentials (:code cause)))
(reset! error (tr "errors.wrong-credentials"))
:else
(reset! error (tr "errors.generic"))))
on-success-default
(fn [data]
(when-let [token (:invitation-token data)]
(st/emit! (rt/nav :auth-verify-token {} {:token token}))))
on-success
(fn [data]
(if (nil? on-success-callback)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -20,7 +20,7 @@
[app.util.timers :as ts]
[beicon.core :as rx]
[goog.functions :as f]
[rumext.alpha :as mf]))
[rumext.v2 :as mf]))
(defn use-id
"Get a stable id value across rerenders."
@ -29,7 +29,7 @@
(defn use-rxsub
[ob]
(let [[state reset-state!] (mf/useState @ob)]
(let [[state reset-state!] (mf/useState #(if (satisfies? IDeref ob) @ob nil))]
(mf/useEffect
(fn []
(let [sub (rx/subscribe ob #(reset-state! %))]
@ -313,3 +313,39 @@
(use-stream stream (partial reset! state))
state))
(defonce ^:private intersection-subject (rx/subject))
(defonce ^:private intersection-observer
(delay (js/IntersectionObserver.
(fn [entries _]
(run! (partial rx/push! intersection-subject) (seq entries)))
#js {:rootMargin "0px"
:threshold 1.0})))
(defn use-visible
[ref & {:keys [once?]}]
(let [[state update-state!] (mf/useState false)]
(mf/with-effect [once?]
(let [node (mf/ref-val ref)
stream (->> intersection-subject
(rx/filter (fn [entry]
(let [target (unchecked-get entry "target")]
(identical? target node))))
(rx/map (fn [entry]
(let [ratio (unchecked-get entry "intersectionRatio")
intersecting? (unchecked-get entry "isIntersecting")]
(or intersecting? (> ratio 0.5)))))
(rx/dedupe))
stream (if once?
(->> stream
(rx/filter identity)
(rx/take 1))
stream)
subs (rx/subscribe stream update-state!)]
(.observe ^js @intersection-observer node)
(fn []
(.unobserve ^js @intersection-observer node)
(rx/dispose! subs))))
state))

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@
(ns app.main.ui.icons
(:refer-clojure :exclude [import mask])
(:require-macros [app.main.ui.icons :refer [icon-xref]])
(:require [rumext.alpha :as mf]))
(:require [rumext.v2 :as mf]))
;; Keep the list of icons sorted

View file

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

View file

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

View file

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

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