Compare commits

...

35 Commits

Author SHA1 Message Date
Andrey Antukh
a1c78683f5 📎 Update version.txt file 2024-08-07 10:57:51 +02:00
Alejandro
4fe77ca386 Merge pull request #4963 from penpot/niwinz-oidc-fixes-2
🐛 Fix OIDC issues and regressions
2024-08-06 12:02:02 +02:00
Andrey Antukh
ea7ad2aaa0 Add flag oidc-registration for switch on/off registration with oidc 2024-08-06 11:51:26 +02:00
Julian Schacher
0162451205 Revert "🐛 Set proper default tenant on exporter"
This reverts commit 86b2ce4dab.
2024-08-06 10:15:21 +02:00
Andrey Antukh
82ad240053 Merge pull request #4960 from penpot/superalex-fix-custom-smtp-port-with-ssl-enabled
🐛 Fix custom smtp port with ssl enabled
2024-08-05 13:01:17 +02:00
Alejandro Alonso
aa21430a5c 🐛 Fix custom smtp port with ssl enabled 2024-08-05 12:45:05 +02:00
Andrey Antukh
aa4368f97f Merge pull request #4959 from penpot/superalex-fix-user-language-validator
🐛 Fix user language validator
2024-08-05 12:35:32 +02:00
Alejandro Alonso
8eddcd64f1 🐛 Fix user language validator 2024-08-05 11:05:56 +02:00
Pablo Alba
fcf9444b1d Merge pull request #4948 from penpot/superalex-a-b-testing-signup-01
 Add a/b testing for signup image
2024-08-01 10:28:43 +02:00
Alejandro Alonso
5ac6f04857 Add a/b testing for signup image 2024-07-31 13:04:12 +02:00
Alejandro
b4d91b5a48 Merge pull request #4937 from penpot/niwinz-fix-email-complains-handling
 Add improvements to internal sns handler
2024-07-31 12:18:01 +02:00
Andrey Antukh
52425a993a 🐛 Check complaints reports in the same way as bounces are checked 2024-07-31 12:02:42 +02:00
Alejandro
e72e812166 Merge pull request #4943 from penpot/niwinz-temporal-log
 Add temporal log entry for profile insert conflict
2024-07-31 10:45:48 +02:00
Pablo Alba
65a00aa13f Merge pull request #4931 from penpot/hiru-fix-touched-detach
🐛 Fix touched groups when detaching with nested copies
2024-07-31 09:46:16 +02:00
Andrey Antukh
acc0623219 Add temporal log entry for profile insert conflict 2024-07-30 16:46:38 +02:00
Andrés Moya
990a948bcc 🐛 Fix touched groups when detaching with nested copies 2024-07-30 14:28:37 +02:00
Andrey Antukh
e0f2c4e0aa Add the ability to pass body to a log entry 2024-07-29 16:16:39 +02:00
Alejandro
4b6d3546e0 Merge pull request #4926 from penpot/niwinz-fix-error-report
🐛 Fix regression on error reporting
2024-07-26 08:27:37 +02:00
Alejandro
0bd3d80816 Merge pull request #4925 from penpot/niwinz-resolve-thumbnail-on-frontend
 Resolve file thumbnail on frontend instead of backend
2024-07-26 08:24:29 +02:00
Andrey Antukh
a261a57868 Prevent double error asignation on persistence error 2024-07-25 17:17:49 +02:00
Andrey Antukh
af389fe63a 🐛 Fix error reporting regression 2024-07-25 17:17:49 +02:00
Andrey Antukh
defcef3e59 Resolve file thumbnail on frontend instead of backend 2024-07-25 15:17:41 +02:00
Andrey Antukh
5ed49995f0 📎 Update changelog 2024-07-25 10:48:46 +02:00
Andrey Antukh
482901f315 Merge pull request #4922 from penpot/niwinz-staging-inet
 Ip Addr parsing and audit log context forwarding fixes
2024-07-24 23:16:27 +02:00
Andrey Antukh
cb26f341d5 Merge pull request #4921 from penpot/eva-fix-search-label
🐛  Fix search label in translations
2024-07-24 21:27:58 +02:00
Andrey Antukh
69b432eb0e 📎 Fix audit event type naming on oidc 2024-07-24 21:25:55 +02:00
Andrey Antukh
7df9ac5e4f 🐛 Fix audit context forwarding on explicit events 2024-07-24 21:25:55 +02:00
Andrey Antukh
343f3feed3 Improve ip-addr parsing 2024-07-24 21:07:11 +02:00
Andrey Antukh
08c8c47006 Merge pull request #4917 from penpot/superalex-fix-flag-email-verification
🐛 Fix flag email verification
2024-07-24 21:00:29 +02:00
Alejandro Alonso
a6d738f0db 🐛 Fix flag email verification 2024-07-24 20:46:49 +02:00
Eva Marco
1f80827d94 🐛 Fix search label in translations 2024-07-24 17:16:13 +02:00
Alejandro
51611fbc09 Merge pull request #4871 from penpot/palba-fix-collapse-groups
🐛 Fix can't collapse colors and typograhies groups when searching assets
2024-07-24 10:20:42 +02:00
Alejandro
c80b35e3ad Merge pull request #4916 from penpot/palba-consolidate-templates-order
 Consolidate templates new order and naming
2024-07-24 09:32:44 +02:00
Pablo Alba
166b8c806c 🐛 Fix can't collapse colors and typograhies groups when searching assets 2024-07-24 09:25:26 +02:00
Pablo Alba
81bd30a11b Consolidate templates new order and naming 2024-07-24 09:18:16 +02:00
50 changed files with 830 additions and 451 deletions

View File

@@ -1,10 +1,25 @@
# CHANGELOG
## 2.1.1
## 2.1.2
### :bug: Bugs fixed
- User switch language to "zh_hant" will get 400 [Github #4884](https://github.com/penpot/penpot/issues/4884)
- Smtp config ignoring port if ssl is set [Github #4872](https://github.com/penpot/penpot/issues/4872)
- Ability to let users to authenticate with a private oidc provider only [Github #4963](https://github.com/penpot/penpot/issues/4963)
## 2.1.1
### :sparkles: New features
- Consolidate templates new order and naming [Taiga #8392](https://tree.taiga.io/project/penpot/task/8392)
### :bug: Bugs fixed
- Fix the “search” label in translations [Taiga #8402](https://tree.taiga.io/project/penpot/issue/8402)
- Fix pencil loader [Taiga #8348](https://tree.taiga.io/project/penpot/issue/8348)
- Fix several issues on the OIDC.
- Fix regression on the `email-verification` flag [Taiga #8398](https://tree.taiga.io/project/penpot/issue/8398)
## 2.1.0 - Things can only get better!

View File

@@ -1,4 +1,16 @@
[{:id "tutorial-for-beginners"
[{:id "wireframing-kit"
:name "Wireframe library"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/wireframing-kit.penpot"}
{:id "prototype-examples"
:name "Prototipe template"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/prototype-examples.penpot"}
{:id "plants-app"
:name "UI mockup example"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Plants-app.penpot"}
{:id "penpot-design-system"
:name "Design system example"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Penpot-Design-system.penpot"}
{:id "tutorial-for-beginners"
:name "Tutorial for beginners"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/tutorial-for-beginners.penpot"}
{:id "lucide-icons"
@@ -7,12 +19,6 @@
{:id "font-awesome"
:name "Font Awesome"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Font-Awesome.penpot"}
{:id "plants-app"
:name "Plants app"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Plants-app.penpot"}
{:id "wireframing-kit"
:name "Wireframing Kit"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/wireframing-kit.penpot"}
{:id "black-white-mobile-templates"
:name "Black & White Mobile Templates"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Black-White-Mobile-Templates.penpot"}
@@ -30,10 +36,4 @@
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Open-Color-Scheme.penpot"}
{:id "flex-layout-playground"
:name "Flex Layout Playground"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Flex-Layout-Playground.penpot"}
{:id "prototype-examples"
:name "Prototipe template"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/prototype-examples.penpot"}
{:id "penpot-design-system"
:name "Design system example"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Penpot-Design-system.penpot"}]
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Flex-Layout-Playground.penpot"}]

View File

@@ -26,6 +26,7 @@
[app.rpc.commands.profile :as profile]
[app.setup :as-alias setup]
[app.tokens :as tokens]
[app.util.inet :as inet]
[app.util.json :as json]
[app.util.time :as dt]
[buddy.sign.jwk :as jwk]
@@ -571,10 +572,10 @@
props (audit/profile->props profile)
context (d/without-nils {:external-session-id (:external-session-id info)})]
(audit/submit! cfg {::audit/type "command"
(audit/submit! cfg {::audit/type "action"
::audit/name "login-with-oidc"
::audit/profile-id (:id profile)
::audit/ip-addr (audit/parse-client-ip request)
::audit/ip-addr (inet/parse-request request)
::audit/props props
::audit/context context})
@@ -591,7 +592,8 @@
:else
(let [info (assoc info :is-active (provider-has-email-verified? cfg info))]
(if (contains? cf/flags :registration)
(if (or (contains? cf/flags :registration)
(contains? cf/flags :oidc-registration))
(redirect-to-register cfg info request)
(redirect-with-error "registration-disabled")))))

View File

@@ -306,6 +306,8 @@
(let [session (create-smtp-session cfg)]
(with-open [transport (.getTransport session (if (::ssl cfg) "smtps" "smtp"))]
(.connect ^Transport transport
^String (::host cfg)
^String (::port cfg)
^String (::username cfg)
^String (::password cfg))
@@ -448,3 +450,11 @@
{:email email :type "bounce"}
{:limit 10}))]
(>= (count reports) threshold))))
(defn has-reports?
([conn email] (has-reports? conn email nil))
([conn email {:keys [threshold] :or {threshold 1}}]
(let [reports (db/exec! conn (sql/select :global-complaint-report
{:email email}
{:limit 10}))]
(>= (count reports) threshold))))

View File

@@ -150,8 +150,8 @@
[["" {:middleware [[mw/server-timing]
[mw/params]
[mw/format-response]
[mw/errors errors/handle]
[mw/parse-request]
[mw/errors errors/handle]
[session/soft-auth cfg]
[actoken/soft-auth cfg]
[mw/restrict-methods]]}

View File

@@ -9,6 +9,7 @@
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.pprint :as pp]
[app.db :as db]
[app.db.sql :as sql]
[app.http.client :as http]
@@ -16,10 +17,10 @@
[app.setup :as-alias setup]
[app.tokens :as tokens]
[app.worker :as-alias wrk]
[clojure.data.json :as j]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]
[jsonista.core :as j]
[promesa.exec :as px]
[ring.request :as rreq]
[ring.response :as-alias rres]))
@@ -136,83 +137,110 @@
(defn- parse-json
[v]
(ex/ignoring
(j/read-value v)))
(try
(j/read-str v)
(catch Throwable cause
(l/wrn :hint "unable to decode request body"
:cause cause))))
(defn- register-bounce-for-profile
[{:keys [::db/pool]} {:keys [type kind profile-id] :as report}]
(when (= kind "permanent")
(db/with-atomic [conn pool]
(db/insert! conn :profile-complaint-report
(try
(db/insert! pool :profile-complaint-report
{:profile-id profile-id
:type (name type)
:content (db/tjson report)})
;; TODO: maybe also try to find profiles by mail and if exists
;; register profile reports for them?
(doseq [recipient (:recipients report)]
(db/insert! conn :global-complaint-report
{:email (:email recipient)
:type (name type)
:content (db/tjson report)}))
(catch Throwable cause
(l/warn :hint "unable to persist profile complaint"
:cause cause)))
(let [profile (db/exec-one! conn (sql/select :profile {:id profile-id}))]
(when (some #(= (:email profile) (:email %)) (:recipients report))
;; If the report matches the profile email, this means that
;; the report is for itself, can be caused when a user
;; registers with an invalid email or the user email is
;; permanently rejecting receiving the email. In this case we
;; have no option to mark the user as muted (and in this case
;; the profile will be also inactive.
(db/update! conn :profile
{:is-muted true}
{:id profile-id}))))))
(defn- register-complaint-for-profile
[{:keys [::db/pool]} {:keys [type profile-id] :as report}]
(db/with-atomic [conn pool]
(db/insert! conn :profile-complaint-report
{:profile-id profile-id
:type (name type)
:content (db/tjson report)})
;; TODO: maybe also try to find profiles by email and if exists
;; register profile reports for them?
(doseq [email (:recipients report)]
(db/insert! conn :global-complaint-report
{:email email
(doseq [recipient (:recipients report)]
(db/insert! pool :global-complaint-report
{:email (:email recipient)
:type (name type)
:content (db/tjson report)}))
(let [profile (db/exec-one! conn (sql/select :profile {:id profile-id}))]
(when (some #(= % (:email profile)) (:recipients report))
(let [profile (db/exec-one! pool (sql/select :profile {:id profile-id}))]
(when (some #(= (:email profile) (:email %)) (:recipients report))
;; If the report matches the profile email, this means that
;; the report is for itself, rare case but can happen; In this
;; case just mark profile as muted (very rare case).
(db/update! conn :profile
;; the report is for itself, can be caused when a user
;; registers with an invalid email or the user email is
;; permanently rejecting receiving the email. In this case we
;; have no option to mark the user as muted (and in this case
;; the profile will be also inactive.
(l/inf :hint "mark profile: muted"
:profile-id (str (:id profile))
:email (:email profile)
:reason "bounce report"
:report-id (:feedback-id report))
(db/update! pool :profile
{:is-muted true}
{:id profile-id})))))
{:id profile-id}
{::db/return-keys false})))))
(defn- register-complaint-for-profile
[{:keys [::db/pool]} {:keys [type profile-id] :as report}]
(try
(db/insert! pool :profile-complaint-report
{:profile-id profile-id
:type (name type)
:content (db/tjson report)})
(catch Throwable cause
(l/warn :hint "unable to persist profile complaint"
:cause cause)))
;; TODO: maybe also try to find profiles by email and if exists
;; register profile reports for them?
(doseq [email (:recipients report)]
(db/insert! pool :global-complaint-report
{:email email
:type (name type)
:content (db/tjson report)}))
(let [profile (db/exec-one! pool (sql/select :profile {:id profile-id}))]
(when (some #(= % (:email profile)) (:recipients report))
;; If the report matches the profile email, this means that
;; the report is for itself, rare case but can happen; In this
;; case just mark profile as muted (very rare case).
(l/inf :hint "mark profile: muted"
:profile-id (str (:id profile))
:email (:email profile)
:reason "complaint report"
:report-id (:feedback-id report))
(db/update! pool :profile
{:is-muted true}
{:id profile-id}
{::db/return-keys false}))))
(defn- process-report
[cfg {:keys [type profile-id] :as report}]
(l/trace :action "processing report" :report (pr-str report))
(cond
;; In this case we receive a bounce/complaint notification without
;; confirmed identity, we just emit a warning but do nothing about
;; it because this is not a normal case. All notifications should
;; come with profile identity.
(nil? profile-id)
(l/warn :msg "a notification without identity received from AWS"
:report (pr-str report))
(l/wrn :hint "not-identified report"
::l/body (pp/pprint-str report {:length 40 :level 6}))
(= "bounce" type)
(register-bounce-for-profile cfg report)
(do
(l/trc :hint "bounce report"
::l/body (pp/pprint-str report {:length 40 :level 6}))
(register-bounce-for-profile cfg report))
(= "complaint" type)
(register-complaint-for-profile cfg report)
(do
(l/trc :hint "complaint report"
::l/body (pp/pprint-str report {:length 40 :level 6}))
(register-complaint-for-profile cfg report))
:else
(l/warn :msg "unrecognized report received from AWS"
:report (pr-str report))))
(l/wrn :hint "unrecognized report"
::l/body (pp/pprint-str report {:length 20 :level 4}))))

View File

@@ -14,32 +14,28 @@
[app.http :as-alias http]
[app.http.access-token :as-alias actoken]
[app.http.session :as-alias session]
[app.util.inet :as inet]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[ring.request :as rreq]
[ring.response :as rres]))
(defn- parse-client-ip
[request]
(or (some-> (rreq/get-header request "x-forwarded-for") (str/split ",") first)
(rreq/get-header request "x-real-ip")
(rreq/remote-addr request)))
(defn request->context
"Extracts error report relevant context data from request."
[request]
(let [claims (-> {}
(into (::session/token-claims request))
(into (::actoken/token-claims request)))]
{:request/path (:path request)
:request/method (:method request)
:request/params (:params request)
:request/user-agent (rreq/get-header request "user-agent")
:request/ip-addr (parse-client-ip request)
:request/ip-addr (inet/parse-request request)
:request/profile-id (:uid claims)
:version/frontend (or (rreq/get-header request "x-frontend-version") "unknown")
:version/backend (:full cf/version)}))
(defmulti handle-error
(fn [cause _ _]
(-> cause ex-data :type)))

View File

@@ -10,6 +10,7 @@
[app.common.logging :as l]
[app.common.transit :as t]
[app.config :as cf]
[app.http.errors :as errors]
[clojure.data.json :as json]
[cuerdas.core :as str]
[ring.request :as rreq]
@@ -70,12 +71,12 @@
:else
request)))
(handle-error [cause]
(handle-error [cause request]
(cond
(instance? RuntimeException cause)
(if-let [cause (ex-cause cause)]
(handle-error cause)
(throw cause))
(handle-error cause request)
(errors/handle cause request))
(instance? RequestTooBigException cause)
(ex/raise :type :validation
@@ -89,14 +90,14 @@
:cause cause)
:else
(throw cause)))]
(errors/handle cause request)))]
(fn [request]
(if (= (rreq/method request) :post)
(let [request (ex/try! (process-request request))]
(if (ex/exception? request)
(handle-error request)
(handler request)))
(try
(-> request process-request handler)
(catch Throwable cause
(handle-error cause request)))
(handler request)))))
(def parse-request

View File

@@ -21,28 +21,18 @@
[app.rpc :as-alias rpc]
[app.rpc.retry :as rtry]
[app.setup :as-alias setup]
[app.util.inet :as inet]
[app.util.services :as-alias sv]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]
[ring.request :as rreq]))
[integrant.core :as ig]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn parse-client-ip
[request]
(let [ip-addr (or (some-> (rreq/get-header request "x-forwarded-for") (str/split ",") first)
(rreq/get-header request "x-real-ip")
(some-> (rreq/remote-addr request) str))
ip-addr (-> ip-addr
(str/split ":" 2)
(first))]
ip-addr))
(defn extract-utm-params
"Extracts additional data from params and namespace them under
`penpot` ns."
@@ -90,17 +80,20 @@
(remove #(contains? reserved-props (key %))))
props))
(defn params->context
"Extract default context properties from RPC params object"
(defn event-from-rpc-params
"Create a base event skeleton with pre-filled some important
data that can be extracted from RPC params object"
[params]
(d/without-nils
{:external-session-id (::rpc/external-session-id params)
:event-origin (::rpc/external-event-origin params)
:triggered-by (::rpc/handler-name params)}))
(let [context {:external-session-id (::rpc/external-session-id params)
:external-event-origin (::rpc/external-event-origin params)
:triggered-by (::rpc/handler-name params)}]
{::type "action"
::profile-id (::rpc/profile-id params)
::ip-addr (::rpc/ip-addr params)
::context (d/without-nils context)}))
;; --- SPECS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; COLLECTOR
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -167,14 +160,16 @@
(assoc :external-session-id session-id)
(assoc :external-event-origin event-origin)
(assoc :access-token-id (some-> token-id str))
(d/without-nils))]
(d/without-nils))
ip-addr (inet/parse-request request)]
{::type (or (::type resultm)
(::rpc/type cfg))
::name (or (::name resultm)
(::sv/name mdata))
::profile-id profile-id
::ip-addr (some-> request parse-client-ip)
::ip-addr ip-addr
::props props
::context context
@@ -202,7 +197,7 @@
:name (::name event)
:type (::type event)
:profile-id (::profile-id event)
:ip-addr (::ip-addr event "0.0.0.0")
:ip-addr (::ip-addr event)
:context (::context event {})
:props (::props event {})
:source "backend"}
@@ -246,8 +241,7 @@
(assoc :created-at tnow)
(update :tracked-at #(or % tnow))
(assoc :props {})
(assoc :context {})
(assoc :ip-addr "0.0.0.0"))]
(assoc :context {}))]
(append-audit-entry! cfg params)))
(when (and (contains? cf/flags :webhooks)

View File

@@ -29,6 +29,7 @@
[app.rpc.rlimit :as rlimit]
[app.setup :as-alias setup]
[app.storage :as-alias sto]
[app.util.inet :as inet]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
@@ -81,7 +82,9 @@
(defn- get-external-event-origin
[request]
(when-let [origin (rreq/get-header request "x-event-origin")]
(when-not (> (count origin) 256)
(when-not (or (> (count origin) 256)
(= origin "null")
(str/blank? origin))
origin)))
(defn- rpc-handler
@@ -93,11 +96,13 @@
profile-id (or (::session/profile-id request)
(::actoken/profile-id request))
ip-addr (inet/parse-request request)
session-id (get-external-session-id request)
event-origin (get-external-event-origin request)
data (-> params
(assoc ::handler-name handler-name)
(assoc ::ip-addr ip-addr)
(assoc ::request-at (dt/now))
(assoc ::external-session-id session-id)
(assoc ::external-event-origin event-origin)

View File

@@ -14,11 +14,12 @@
[app.config :as cf]
[app.db :as db]
[app.http :as-alias http]
[app.loggers.audit :as audit]
[app.loggers.audit :as-alias audit]
[app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.util.inet :as inet]
[app.util.services :as sv]
[app.util.time :as dt]))
@@ -61,7 +62,7 @@
(defn- handle-events
[{:keys [::db/pool]} {:keys [::rpc/profile-id events] :as params}]
(let [request (-> params meta ::http/request)
ip-addr (audit/parse-client-ip request)
ip-addr (inet/parse-request request)
tnow (dt/now)
xform (comp
(map (fn [event]

View File

@@ -209,7 +209,19 @@
(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"))
(when (eml/has-bounce-reports? cfg (:email params))
(ex/raise :type :restriction
:code :email-has-permanent-bounces
:email (:email params)
:hint "looks like the email has bounce reports"))
(when (eml/has-complaint-reports? cfg (:email params))
(ex/raise :type :restriction
:code :email-has-complaints
:email (:email params)
:hint "looks like the email has complaint reports")))
(defn prepare-register
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}]
@@ -286,14 +298,17 @@
(try
(-> (db/insert! conn :profile params)
(profile/decode-row))
(catch org.postgresql.util.PSQLException e
(let [state (.getSQLState e)]
(catch org.postgresql.util.PSQLException cause
(let [state (.getSQLState cause)]
(if (not= state "23505")
(throw e)
(ex/raise :type :validation
:code :email-already-exists
:hint "email already exists"
:cause e)))))))
(throw cause)
(do
(l/error :hint "not an error" :cause cause)
(ex/raise :type :validation
:code :email-already-exists
:hint "email already exists"
:cause cause))))))))
(defn create-profile-rels!
[conn {:keys [id] :as profile}]
@@ -340,7 +355,7 @@
profile (if-let [profile-id (:profile-id claims)]
(profile/get-profile conn profile-id)
(let [is-active (or (boolean (:is-active params))
(let [is-active (or (boolean (:is-active claims))
(not (contains? cf/flags :email-verification)))
params (-> params
(assoc :is-active is-active)
@@ -348,6 +363,9 @@
(->> (create-profile! conn params)
(create-profile-rels! conn))))
;; When no profile-id comes on claims means a new register
created? (not (:profile-id claims))
invitation (when-let [token (:invitation-token params)]
(tokens/verify (::setup/props cfg) {:token token :iss :team-invitation}))
@@ -385,8 +403,8 @@
;; When a new user is created and it is already activated by
;; configuration or specified by OIDC, we just mark the profile
;; as logged-in
(not (:profile-id claims))
(if (:is-active claims)
created?
(if (:is-active profile)
(-> (profile/strip-private-attrs profile)
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta
@@ -395,20 +413,22 @@
::audit/profile-id (:id profile)}))
(do
(send-email-verification! cfg profile)
(when-not (eml/has-reports? conn (:email profile))
(send-email-verification! cfg profile))
(rph/with-meta {:email (:email profile)}
{::audit/replace-props props
::audit/context {:action "email-verification"}
::audit/profile-id (:id profile)})))
:else
(let [elapsed? (elapsed-verify-threshold? profile)
bounce? (eml/has-bounce-reports? conn (:email profile))
action (if bounce?
"ignore-because-bounce"
(if elapsed?
"resend-email-verification"
"ignore"))]
(let [elapsed? (elapsed-verify-threshold? profile)
complaints? (eml/has-reports? conn (:email profile))
action (if complaints?
"ignore-because-complaints"
(if elapsed?
"resend-email-verification"
"ignore"))]
(l/wrn :hint "repeated registry detected"
:profile-id (str (:id profile))
@@ -443,7 +463,7 @@
;; ---- COMMAND: Request Profile Recovery
(defn- request-profile-recovery
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}]
[{:keys [::db/conn] :as cfg} {:keys [email] :as params}]
(letfn [(create-recovery-token [{:keys [id] :as profile}]
(let [token (tokens/generate (::setup/props cfg)
{:iss :password-recovery
@@ -465,39 +485,42 @@
:extra-data ptoken})
nil))]
(db/with-atomic [conn pool]
(let [profile (->> (profile/clean-email email)
(profile/get-profile-by-email conn))]
(let [profile (->> (profile/clean-email email)
(profile/get-profile-by-email conn))]
(cond
(not profile)
(l/wrn :hint "attempt of profile recovery: no profile found"
:profile-email email)
(cond
(not profile)
(l/wrn :hint "attempt of profile recovery: no profile found"
:profile-email email)
(not (eml/allow-send-emails? conn profile))
(l/wrn :hint "attempt of profile recovery: profile is muted"
:profile-id (str (:id profile))
:profile-email (:email profile))
(not (eml/allow-send-emails? conn profile))
(l/wrn :hint "attempt of profile recovery: profile is muted"
:profile-id (str (:id profile))
:profile-email (:email profile))
(eml/has-bounce-reports? conn (:email profile))
(l/wrn :hint "attempt of profile recovery: email has bounces"
:profile-id (str (:id profile))
:profile-email (:email profile))
(eml/has-bounce-reports? conn (:email profile))
(l/wrn :hint "attempt of profile recovery: email has bounces"
:profile-id (str (:id profile))
:profile-email (:email profile))
(not (elapsed-verify-threshold? profile))
(l/wrn :hint "attempt of profile recovery: retry attempt threshold not elapsed"
:profile-id (str (:id profile))
:profile-email (:email profile))
(eml/has-complaint-reports? conn (:email profile))
(l/wrn :hint "attempt of profile recovery: email has complaints"
:profile-id (str (:id profile))
:profile-email (:email profile))
(not (elapsed-verify-threshold? profile))
(l/wrn :hint "attempt of profile recovery: retry attempt threshold not elapsed"
:profile-id (str (:id profile))
:profile-email (:email profile))
:else
(do
(db/update! conn :profile
{:modified-at (dt/now)}
{:id (:id profile)})
(->> profile
(create-recovery-token)
(send-email-notification conn))))))))
:else
(do
(db/update! conn :profile
{:modified-at (dt/now)}
{:id (:id profile)})
(->> profile
(create-recovery-token)
(send-email-notification conn)))))))
(def schema:request-profile-recovery
@@ -509,6 +532,6 @@
::doc/added "1.15"
::sm/params schema:request-profile-recovery}
[cfg params]
(request-profile-recovery cfg params))
(db/tx-run! cfg request-profile-recovery params))

View File

@@ -671,7 +671,7 @@
f.modified_at,
f.name,
f.is_shared,
ft.media_id,
ft.media_id AS thumbnail_id,
row_number() over w as row_num
from file as f
inner join project as p on (p.id = f.project_id)
@@ -690,10 +690,8 @@
[conn team-id]
(->> (db/exec! conn [sql:team-recent-files team-id])
(mapv (fn [row]
(if-let [media-id (:media-id row)]
(-> row
(dissoc :media-id)
(assoc :thumbnail-uri (resolve-public-uri media-id)))
(if-let [media-id (:thumbnail-id row)]
(assoc row :thumbnail-uri (resolve-public-uri media-id))
(dissoc row :media-id))))))
(def ^:private schema:get-team-recent-files

View File

@@ -406,4 +406,5 @@
(when-not (db/read-only? conn)
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)
media (create-file-thumbnail! cfg params)]
{:uri (files/resolve-public-uri (:id media))})))))
{:uri (files/resolve-public-uri (:id media))
:id (:id media)})))))

View File

@@ -413,15 +413,13 @@
{:modified-at (dt/now)}
{:id project-id})
(let [props (audit/clean-props params)
context (audit/params->context params)]
(let [props (audit/clean-props params)]
(doseq [file-id result]
(audit/submit! cfg
{::audit/type "action"
::audit/name "create-file"
::audit/profile-id profile-id
::audit/props (assoc props :id file-id)
::audit/context context})))
(let [props (assoc props :id file-id)
event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name "create-file")
(assoc ::audit/props props))]
(audit/submit! cfg event))))
result))))

View File

@@ -102,7 +102,7 @@
(sm/define
[:map {:title "update-profile"}
[:fullname [::sm/word-string {:max 250}]]
[:lang {:optional true} [:string {:max 5}]]
[:lang {:optional true} [:string {:max 8}]]
[:theme {:optional true} [:string {:max 250}]]]))
(sv/defmethod ::update-profile
@@ -276,19 +276,19 @@
(sv/defmethod ::request-email-change
{::doc/added "1.0"
::sm/params schema:request-email-change}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id email] :as params}]
(db/with-atomic [conn pool]
(let [profile (db/get-by-id conn :profile profile-id)
cfg (assoc cfg ::conn conn)
params (assoc params
:profile profile
:email (clean-email email))]
(if (contains? cf/flags :smtp)
(request-email-change! cfg params)
(change-email-immediately! cfg params)))))
[cfg {:keys [::rpc/profile-id email] :as params}]
(db/tx-run! cfg
(fn [cfg]
(let [profile (db/get-by-id cfg :profile profile-id)
params (assoc params
:profile profile
:email (clean-email email))]
(if (contains? cf/flags :smtp)
(request-email-change! cfg params)
(change-email-immediately! cfg params))))))
(defn- change-email-immediately!
[{:keys [::conn]} {:keys [profile email] :as params}]
[{:keys [::db/conn]} {:keys [profile email] :as params}]
(when (not= email (:email profile))
(check-profile-existence! conn params))
@@ -299,7 +299,7 @@
{:changed true})
(defn- request-email-change!
[{:keys [::conn] :as cfg} {:keys [profile email] :as params}]
[{:keys [::db/conn] :as cfg} {:keys [profile email] :as params}]
(let [token (tokens/generate (::setup/props cfg)
{:iss :change-email
:exp (dt/in-future "15m")
@@ -319,9 +319,28 @@
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
(when (eml/has-bounce-reports? conn email)
(ex/raise :type :validation
(ex/raise :type :restriction
:code :email-has-permanent-bounces
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
:email email
:hint "looks like the email has bounce reports"))
(when (eml/has-complaint-reports? conn email)
(ex/raise :type :restriction
:code :email-has-complaints
:email email
:hint "looks like the email has spam complaint reports"))
(when (eml/has-bounce-reports? conn (:email profile))
(ex/raise :type :restriction
:code :email-has-permanent-bounces
:email (:email profile)
:hint "looks like the email has bounce reports"))
(when (eml/has-complaint-reports? conn (:email profile))
(ex/raise :type :restriction
:code :email-has-complaints
:email (:email profile)
:hint "looks like the email has spam complaint reports"))
(eml/send! {::eml/conn conn
::eml/factory eml/change-email

View File

@@ -734,12 +734,19 @@
:email email
:hint "the profile has reported repeatedly as spam or has bounces"))
;; Secondly check if the invited member email is part of the global spam/bounce report.
;; Secondly check if the invited member email is part of the global bounce report.
(when (eml/has-bounce-reports? conn email)
(ex/raise :type :validation
(ex/raise :type :restriction
:code :email-has-permanent-bounces
:email email
:hint "the email you invite has been repeatedly reported as spam or bounce"))
:hint "the email you invite has been repeatedly reported as bounce"))
;; Secondly check if the invited member email is part of the global complain report.
(when (eml/has-complaint-reports? conn email)
(ex/raise :type :restriction
:code :email-has-complaints
:email email
:hint "the email you invite has been repeatedly reported as spam"))
;; When we have email verification disabled and invitation user is
;; already present in the database, we proceed to add it to the
@@ -787,18 +794,15 @@
(l/info :hint "invitation token" :token itoken))
(let [props (-> (dissoc tprops :profile-id)
(audit/clean-props))
context (audit/params->context params)]
(audit/submit! cfg
{::audit/type "action"
::audit/name (if updated?
"update-team-invitation"
"create-team-invitation")
::audit/profile-id (:id profile)
::audit/props props
::audit/context context}))
(let [props (-> (dissoc tprops :profile-id)
(audit/clean-props))
evname (if updated?
"update-team-invitation"
"create-team-invitation")
event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name evname)
(assoc ::audit/props props))]
(audit/submit! cfg event))
(eml/send! {::eml/conn conn
::eml/factory eml/invite-to-team
@@ -882,62 +886,51 @@
(sv/defmethod ::create-team-with-invitations
{::doc/added "1.17"
::sm/params schema:create-team-with-invitations}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id emails role name] :as params}]
(db/with-atomic [conn pool]
[cfg {:keys [::rpc/profile-id emails role name] :as params}]
(let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params)))
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params)))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))
cfg (assoc cfg ::db/conn conn)
team (create-team cfg params)
profile (db/get-by-id conn :profile profile-id)
emails (into #{} (map profile/clean-email) emails)
context (audit/params->context params)]
cfg (assoc cfg ::db/conn conn)
team (create-team cfg params)
profile (db/get-by-id conn :profile profile-id)
emails (into #{} (map profile/clean-email) emails)]
;; Create invitations for all provided emails.
(->> emails
(map (fn [email]
(-> params
(assoc :team team)
(assoc :profile profile)
(assoc :email email)
(assoc :role role))))
(run! (partial create-invitation cfg)))
(let [props {:name name :features features}
event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name "create-team")
(assoc ::audit/props props))]
(audit/submit! cfg event))
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/teams-per-profile
::quotes/profile-id profile-id}
{::quotes/id ::quotes/invitations-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}))
;; Create invitations for all provided emails.
(->> emails
(map (fn [email]
(-> params
(assoc :team team)
(assoc :profile profile)
(assoc :email email)
(assoc :role role))))
(run! (partial create-invitation cfg)))
(audit/submit! cfg
{::audit/type "action"
::audit/name "create-team"
::audit/profile-id profile-id
::audit/props {:name name
:features features}
::audit/context context})
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/teams-per-profile
::quotes/profile-id profile-id}
{::quotes/id ::quotes/invitations-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}))
(audit/submit! cfg
{::audit/type "command"
::audit/name "create-team-invitations"
::audit/profile-id profile-id
::audit/props {:emails emails
:role role
:profile-id profile-id
:invitations (count emails)}})
(vary-meta team assoc ::audit/props {:invitations (count emails)}))))
(vary-meta team assoc ::audit/props {:invitations (count emails)})))))
;; --- Query: get-team-invitation-token

View File

@@ -169,19 +169,15 @@
;; 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 [context (audit/params->context params)
props {:team-id (:team-id claims)
:role (:role claims)
:invitation-id (:id invitation)}]
(let [props {:team-id (:team-id claims)
:role (:role claims)
:invitation-id (:id invitation)}
event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name "accept-team-invitation")
(assoc ::audit/props props))]
(accept-invitation cfg claims invitation profile)
(audit/submit! cfg
{::audit/type "action"
::audit/name "accept-team-invitation"
::audit/profile-id profile-id
::audit/props props
::audit/context context})
(audit/submit! cfg event)
(assoc claims :state :created))
(ex/raise :type :validation

View File

@@ -51,12 +51,12 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.http :as-alias http]
[app.loggers.audit :refer [parse-client-ip]]
[app.redis :as rds]
[app.redis.script :as-alias rscript]
[app.rpc :as-alias rpc]
[app.rpc.helpers :as rph]
[app.rpc.rlimit.result :as-alias lresult]
[app.util.inet :as inet]
[app.util.services :as-alias sv]
[app.util.time :as dt]
[app.worker :as wrk]
@@ -215,7 +215,7 @@
[{:keys [::rpc/profile-id] :as params}]
(let [request (-> params meta ::http/request)]
(or profile-id
(some-> request parse-client-ip)
(some-> request inet/parse-request)
uuid/zero)))
(defn process-request!

View File

@@ -0,0 +1,37 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.util.inet
"INET addr parsing and validation helpers"
(:require
[cuerdas.core :as str]
[ring.request :as rreq])
(:import
com.google.common.net.InetAddresses
java.net.InetAddress))
(defn valid?
[s]
(InetAddresses/isInetAddress s))
(defn normalize
[s]
(try
(let [addr (InetAddresses/forString s)]
(.getHostAddress ^InetAddress addr))
(catch Throwable _cause
nil)))
(defn parse-request
[request]
(or (some-> (rreq/get-header request "x-real-ip")
(normalize))
(some-> (rreq/get-header request "x-forwarded-for")
(str/split #"\s*,\s*")
(first)
(normalize))
(some-> (rreq/remote-addr request)
(normalize))))

View File

@@ -11,7 +11,7 @@
[app.common.logging :as l]
[app.common.transit :as t]
[app.common.uuid :as uuid]
[app.loggers.audit :refer [parse-client-ip]]
[app.util.inet :as inet]
[app.util.time :as dt]
[promesa.exec :as px]
[promesa.exec.csp :as sp]
@@ -84,7 +84,7 @@
output-ch (sp/chan :buf output-buff-size)
hbeat-ch (sp/chan :buf (sp/sliding-buffer 6))
close-ch (sp/chan)
ip-addr (parse-client-ip request)
ip-addr (inet/parse-request request)
uagent (rreq/get-header request "user-agent")
id (uuid/next)
state (atom {})

View File

@@ -28,7 +28,8 @@
ring.request/Request
(get-header [_ name]
(case name
"x-forwarded-for" "127.0.0.44"))))
"x-forwarded-for" "127.0.0.44"
"x-real-ip" "127.0.0.43"))))
(t/deftest push-events-1
(with-redefs [app.config/flags #{:audit-log}]
@@ -46,6 +47,7 @@
:profile-id (:id prof)
:timestamp (dt/now)
:type "action"}]}
params (with-meta params
{:app.http/request http-request})

View File

@@ -590,9 +590,10 @@
(th/create-global-complaint-for pool {:type :bounce :email "user@example.com"})
(let [out (th/command! data)]
(t/is (th/success? out))
(let [result (:result out)]
(t/is (contains? result :token))))))
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :restriction (:type edata)))
(t/is (= :email-has-permanent-bounces (:code edata)))))))
(t/deftest register-profile-with-complained-email
(let [pool (:app.db/pool th/*system*)
@@ -603,9 +604,11 @@
(th/create-global-complaint-for pool {:type :complaint :email "user@example.com"})
(let [out (th/command! data)]
(t/is (th/success? out))
(let [result (:result out)]
(t/is (contains? result :token))))))
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :restriction (:type edata)))
(t/is (= :email-has-complaints (:code edata)))))))
(t/deftest register-profile-with-email-as-password
(let [data {::th/type :prepare-register-profile
@@ -636,20 +639,26 @@
;; with complaints
(th/create-global-complaint-for pool {:type :complaint :email (:email data)})
(let [out (th/command! data)]
(let [out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (= 2 (:call-count @mock))))
(let [edata (-> out :error ex-data)]
(t/is (= :restriction (:type edata)))
(t/is (= :email-has-complaints (:code edata))))
(t/is (= 1 (:call-count @mock))))
;; with bounces
(th/create-global-complaint-for pool {:type :bounce :email (:email data)})
(let [out (th/command! data)
error (:error out)]
(let [out (th/command! data)]
;; (th/print-result! out)
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :email-has-permanent-bounces))
(t/is (= 2 (:call-count @mock)))))))
(let [edata (-> out :error ex-data)]
(t/is (= :restriction (:type edata)))
(t/is (= :email-has-permanent-bounces (:code edata))))
(t/is (= 1 (:call-count @mock)))))))
(t/deftest email-change-request-without-smtp
@@ -714,7 +723,7 @@
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (= 2 (:call-count @mock))))
(t/is (= 1 (:call-count @mock))))
;; with valid email and active user with global bounce
(th/create-global-complaint-for pool {:type :bounce :email (:email profile2)})
@@ -723,7 +732,7 @@
(t/is (nil? (:result out)))
(t/is (nil? (:error out)))
;; (th/print-result! out)
(t/is (= 2 (:call-count @mock))))))))
(t/is (= 1 (:call-count @mock))))))))
(t/deftest update-profile-password

View File

@@ -62,8 +62,8 @@
(th/reset-mock! mock)
(let [data (assoc data :emails ["foo@bar.com"])
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count (deref mock)))))
(t/is (not (th/success? out)))
(t/is (= 0 (:call-count (deref mock)))))
;; get invitation token
(let [params {::th/type :get-team-invitation-token
@@ -86,7 +86,7 @@
(t/is (= 0 (:call-count @mock)))
(let [edata (-> out :error ex-data)]
(t/is (= :validation (:type edata)))
(t/is (= :restriction (:type edata)))
(t/is (= :email-has-permanent-bounces (:code edata)))))
;; invite internal user that is muted

View File

@@ -670,52 +670,14 @@
(ctyl/delete-typography data id))
;; === Operations
(defmethod process-operation :set
[on-changed shape op]
(let [attr (:attr op)
group (get ctk/sync-attrs attr)
val (:val op)
shape-val (get shape attr)
ignore (or (:ignore-touched op) (= attr :position-data)) ;; position-data is a derived attribute and
ignore-geometry (:ignore-geometry op) ;; never triggers touched by itself
is-geometry? (and (or (= group :geometry-group)
(and (= group :content-group) (= (:type shape) :path)))
(not (#{:width :height} attr))) ;; :content in paths are also considered geometric
;; TODO: the check of :width and :height probably may be removed
;; after the check added in data/workspace/modifiers/check-delta
;; function. Better check it and test toroughly when activating
;; components-v2 mode.
in-copy? (ctk/in-component-copy? shape)
;; For geometric attributes, there are cases in that the value changes
;; slightly (e.g. when rounding to pixel, or when recalculating text
;; positions in different zoom levels). To take this into account, we
;; ignore geometric changes smaller than 1 pixel.
equal? (if is-geometry?
(gsh/close-attrs? attr val shape-val 1)
(gsh/close-attrs? attr val shape-val))]
;; Notify when value has changed, except when it has not moved relative to the
;; component head.
(when (and group (not equal?) (not (and ignore-geometry is-geometry?)))
(on-changed shape))
(cond-> shape
;; Depending on the origin of the attribute change, we need or not to
;; set the "touched" flag for the group the attribute belongs to.
;; In some cases we need to ignore touched only if the attribute is
;; geometric (position, width or transformation).
(and in-copy? group (not ignore) (not equal?)
(not (and ignore-geometry is-geometry?)))
(-> (update :touched cfh/set-touched-group group)
(dissoc :remote-synced))
(nil? val)
(dissoc attr)
(some? val)
(assoc attr val))))
(ctn/set-shape-attr shape
(:attr op)
(:val op)
:on-changed on-changed
:ignore-touched (:ignore-touched op)
:ignore-geometry (:ignore-geometry op)))
(defmethod process-operation :set-touched
[_ shape op]

View File

@@ -357,15 +357,6 @@
;; COMPONENTS HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn set-touched-group
[touched group]
(when group
(conj (or touched #{}) group)))
(defn touched-group?
[shape group]
((or (:touched shape) #{}) group))
(defn make-container
[page-or-component type]
(assoc page-or-component :type type))

View File

@@ -153,14 +153,29 @@
(defn build-message
[props]
(loop [props (seq props)
result []]
result []
body nil]
(if-let [[k v] (first props)]
(if (simple-ident? k)
(cond
(simple-ident? k)
(recur (next props)
(conj result (str (name k) "=" (pr-str v))))
(conj result (str (name k) "=" (pr-str v)))
body)
(= ::body k)
(recur (next props)
result))
(str/join ", " result))))
result
v)
:else
(recur (next props)
result
body))
(let [message (str/join ", " result)]
(if (string? body)
(str message "\n" body)
message)))))
(defn build-stack-trace
[cause]

View File

@@ -288,13 +288,23 @@
(some? (:shape-ref ref-shape))
(pcb/update-shapes [(:id shape)] #(assoc % :shape-ref (:shape-ref ref-shape)))
;; When advancing level, if the referenced shape has a swap slot, it must be
;; copied to the current shape, because the shape-ref now will not be pointing
;; to a near main (except for first level subcopies).
;; When advancing level, the normal touched groups (not swap slots) of the
;; ref-shape must be merged into the current shape, because they refer to
;; the new referenced shape.
(some? ref-shape)
(pcb/update-shapes
[(:id shape)]
#(assoc % :touched
(clojure.set/union (:touched shape)
(ctk/normal-touched-groups ref-shape))))
;; Swap slot must also be copied if the current shape has not any,
;; except if this is the first level subcopy.
(and (some? (ctk/get-swap-slot ref-shape))
(nil? (ctk/get-swap-slot shape))
(not= (:id shape) shape-id))
(pcb/update-shapes [(:id shape)] #(ctk/set-swap-slot % (ctk/get-swap-slot ref-shape))))))]
(reduce skip-near changes children)))
(defn prepare-restore-component

View File

@@ -12,6 +12,7 @@
[app.common.test-helpers.ids-map :as thi]
[app.common.types.color :as ctc]
[app.common.types.colors-list :as ctcl]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.pages-list :as ctpl]
[app.common.types.shape :as cts]
@@ -69,6 +70,19 @@
(thf/current-page file))]
(ctst/get-shape page id)))
(defn update-shape
[file shape-label attr val & {:keys [page-label]}]
(let [page (if page-label
(thf/get-page file page-label)
(thf/current-page file))
shape (ctst/get-shape page (thi/id shape-label))]
(ctf/update-file-data
file
(fn [file-data]
(ctpl/update-page file-data
(:id page)
#(ctst/set-shape % (ctn/set-shape-attr shape attr val)))))))
(defn sample-color
[label & {:keys [] :as params}]
(ctc/make-color (assoc params :id (thi/new-id! label))))

View File

@@ -202,6 +202,11 @@
[group]
(str/starts-with? (name group) "swap-slot-"))
(defn normal-touched-groups
"Gets all touched groups that are not swap slots."
[shape]
(into #{} (remove swap-slot? (:touched shape))))
(defn group->swap-slot
[group]
(uuid/uuid (subs (name group) 10)))

View File

@@ -498,7 +498,7 @@
; original component doesn't exist or is deleted. So for this function purposes, they
; are removed from the list
remove? (fn [shape]
(let [component (get-in libraries [(:component-file shape) :data :components (:component-id shape)])]
(let [component (get-in libraries [(:component-file shape) :data :components (:component-id shape)])]
(and component (not (:deleted component)))))
selected-components (cond->> (mapcat collect-main-shapes children objects)
@@ -534,3 +534,48 @@
(if (or no-changes? (not (invalid-structure-for-component? objects parent children pasting? libraries)))
[parent-id (get-frame parent-id)]
(recur (:parent-id parent) objects children pasting? libraries))))))
;; --- SHAPE UPDATE
(defn set-shape-attr
[shape attr val & {:keys [on-changed ignore-touched ignore-geometry]}]
(let [group (get ctk/sync-attrs attr)
shape-val (get shape attr)
ignore (or ignore-touched (= attr :position-data)) ;; position-data is a derived attribute and
is-geometry? (and (or (= group :geometry-group) ;; never triggers touched by itself
(and (= group :content-group) (= (:type shape) :path)))
(not (#{:width :height} attr))) ;; :content in paths are also considered geometric
;; TODO: the check of :width and :height probably may be removed
;; after the check added in data/workspace/modifiers/check-delta
;; function. Better check it and test toroughly when activating
;; components-v2 mode.
in-copy? (ctk/in-component-copy? shape)
;; For geometric attributes, there are cases in that the value changes
;; slightly (e.g. when rounding to pixel, or when recalculating text
;; positions in different zoom levels). To take this into account, we
;; ignore geometric changes smaller than 1 pixel.
equal? (if is-geometry?
(gsh/close-attrs? attr val shape-val 1)
(gsh/close-attrs? attr val shape-val))]
;; Notify when value has changed, except when it has not moved relative to the
;; component head.
(when (and on-changed group (not equal?) (not (and ignore-geometry is-geometry?)))
(on-changed shape))
(cond-> shape
;; Depending on the origin of the attribute change, we need or not to
;; set the "touched" flag for the group the attribute belongs to.
;; In some cases we need to ignore touched only if the attribute is
;; geometric (position, width or transformation).
(and in-copy? group (not ignore) (not equal?)
(not (and ignore-geometry is-geometry?)))
(-> (update :touched ctk/set-touched-group group)
(dissoc :remote-synced))
(nil? val)
(dissoc attr)
(some? val)
(assoc attr val))))

View File

@@ -501,8 +501,8 @@
(assoc :proportion-lock true)))
(defn setup-shape
"A function that initializes the geometric data of
the shape. The props must have :x :y :width :height."
"A function that initializes the geometric data of the shape. The props must
contain at least :x :y :width :height."
[{:keys [type] :as props}]
(let [shape (make-minimal-shape type)

View File

@@ -4,7 +4,7 @@
;;
;; Copyright (c) KALEIDOS INC
(ns common-tests.logic.comp-detach-with-swap-test
(ns common-tests.logic.comp-detach-with-nested-test
(:require
[app.common.files.changes-builder :as pcb]
[app.common.logic.libraries :as cll]
@@ -18,7 +18,7 @@
(t/use-fixtures :each thi/test-fixture)
;; Related .penpot file: common/test/cases/detach-with-swap.penpot
;; Related .penpot file: common/test/cases/detach-with-nested.penpot
(defn- setup-file
[]
;; {:r-ellipse} [:name Ellipse, :type :frame] # [Component :c-ellipse]
@@ -195,3 +195,177 @@
(t/is (= (:shape-ref copy-nested-rectangle) (thi/id :rectangle)))
(t/is (nil? (ctk/get-swap-slot copy-nested-rectangle)))))
(t/deftest test-propagate-touched
(let [;; ==== Setup
file (-> (setup-file)
(ths/update-shape :nested2-ellipse :fills (ths/sample-fills-color :fill-color "#fabada"))
(thc/instantiate-component :c-big-board
:copy-big-board
:children-labels [:copy-h-board-with-ellipse
:copy-nested2-h-ellipse
:copy-nested2-ellipse]))
page (thf/current-page file)
nested2-ellipse (ths/get-shape file :nested2-ellipse)
copy-nested2-ellipse (ths/get-shape file :copy-nested2-ellipse)
;; ==== Action
changes (cll/generate-detach-instance (-> (pcb/empty-changes nil)
(pcb/with-page page)
(pcb/with-objects (:objects page)))
page
{(:id file) file}
(thi/id :copy-big-board))
file' (thf/apply-changes file changes)
;; ==== Get
nested2-ellipse' (ths/get-shape file' :nested2-ellipse)
copy-nested2-ellipse' (ths/get-shape file' :copy-nested2-ellipse)
fills' (:fills copy-nested2-ellipse')
fill' (first fills')]
;; ==== Check
;; The touched group must be propagated to the copy, because now this copy
;; has the original ellipse component as near main, but its attributes have
;; been inherited from the ellipse inside big-board.
(t/is (= (:touched nested2-ellipse) #{:fill-group}))
(t/is (= (:touched copy-nested2-ellipse) nil))
(t/is (= (:touched nested2-ellipse') #{:fill-group}))
(t/is (= (:touched copy-nested2-ellipse') #{:fill-group}))
(t/is (= (count fills') 1))
(t/is (= (:fill-color fill') "#fabada"))
(t/is (= (:fill-opacity fill') 1))))
(t/deftest test-merge-touched
(let [;; ==== Setup
file (-> (setup-file)
(ths/update-shape :nested2-ellipse :fills (ths/sample-fills-color :fill-color "#fabada"))
(thc/instantiate-component :c-big-board
:copy-big-board
:children-labels [:copy-h-board-with-ellipse
:copy-nested2-h-ellipse
:copy-nested2-ellipse])
(ths/update-shape :copy-nested2-ellipse :name "Modified name")
(ths/update-shape :copy-nested2-ellipse :fills (ths/sample-fills-color :fill-color "#abcdef")))
page (thf/current-page file)
nested2-ellipse (ths/get-shape file :nested2-ellipse)
copy-nested2-ellipse (ths/get-shape file :copy-nested2-ellipse)
;; ==== Action
changes (cll/generate-detach-instance (-> (pcb/empty-changes nil)
(pcb/with-page page)
(pcb/with-objects (:objects page)))
page
{(:id file) file}
(thi/id :copy-big-board))
file' (thf/apply-changes file changes)
;; ==== Get
nested2-ellipse' (ths/get-shape file' :nested2-ellipse)
copy-nested2-ellipse' (ths/get-shape file' :copy-nested2-ellipse)
fills' (:fills copy-nested2-ellipse')
fill' (first fills')]
;; ==== Check
;; If the copy have been already touched, merge the groups and preserve the modifications.
(t/is (= (:touched nested2-ellipse) #{:fill-group}))
(t/is (= (:touched copy-nested2-ellipse) #{:name-group :fill-group}))
(t/is (= (:touched nested2-ellipse') #{:fill-group}))
(t/is (= (:touched copy-nested2-ellipse') #{:name-group :fill-group}))
(t/is (= (count fills') 1))
(t/is (= (:fill-color fill') "#abcdef"))
(t/is (= (:fill-opacity fill') 1))))
(t/deftest test-dont-propagete-touched-when-swapped-copy
(let [;; ==== Setup
file (-> (setup-file)
(ths/update-shape :nested-rectangle :fills (ths/sample-fills-color :fill-color "#fabada"))
(thc/instantiate-component :c-big-board
:copy-big-board
:children-labels [:copy-h-board-with-ellipse
:copy-nested2-h-ellipse
:copy-nested2-ellipse])
(thc/component-swap :copy-h-board-with-ellipse
:c-board-with-rectangle
:copy-h-board-with-rectangle
:children-labels [:copy-nested2-h-rectangle
:copy-nested2-rectangle]))
page (thf/current-page file)
nested2-rectangle (ths/get-shape file :nested2-rectangle)
copy-nested2-rectangle (ths/get-shape file :copy-nested2-rectangle)
;; ==== Action
changes (cll/generate-detach-instance (-> (pcb/empty-changes nil)
(pcb/with-page page)
(pcb/with-objects (:objects page)))
page
{(:id file) file}
(thi/id :copy-big-board))
file' (thf/apply-changes file changes)
;; ==== Get
nested2-rectangle' (ths/get-shape file' :nested2-rectangle)
copy-nested2-rectangle' (ths/get-shape file' :copy-nested2-rectangle)
fills' (:fills copy-nested2-rectangle')
fill' (first fills')]
;; ==== Check
;; If the copy has been swapped, there is nothing to propagate since it's already
;; pointing to the swapped near main.
(t/is (= (:touched nested2-rectangle) nil))
(t/is (= (:touched copy-nested2-rectangle) nil))
(t/is (= (:touched nested2-rectangle') nil))
(t/is (= (:touched copy-nested2-rectangle') nil))
(t/is (= (count fills') 1))
(t/is (= (:fill-color fill') "#fabada"))
(t/is (= (:fill-opacity fill') 1))))
(t/deftest test-propagate-touched-when-swapped-main
(let [;; ==== Setup
file (-> (setup-file)
(thc/component-swap :nested2-h-ellipse
:c-rectangle
:nested2-h-rectangle
:children-labels [:nested2-rectangle])
(ths/update-shape :nested2-rectangle :fills (ths/sample-fills-color :fill-color "#fabada"))
(thc/instantiate-component :c-big-board
:copy-big-board
:children-labels [:copy-h-board-with-ellipse
:copy-nested2-h-rectangle
:copy-nested2-rectangle]))
page (thf/current-page file)
nested2-rectangle (ths/get-shape file :nested2-rectangle)
copy-nested2-rectangle (ths/get-shape file :copy-nested2-rectangle)
;; ==== Action
changes (cll/generate-detach-instance (-> (pcb/empty-changes nil)
(pcb/with-page page)
(pcb/with-objects (:objects page)))
page
{(:id file) file}
(thi/id :copy-big-board))
file' (thf/apply-changes file changes)
;; ==== Get
nested2-rectangle' (ths/get-shape file' :nested2-rectangle)
copy-nested2-rectangle' (ths/get-shape file' :copy-nested2-rectangle)
fills' (:fills copy-nested2-rectangle')
fill' (first fills')]
;; ==== Check
;; If the main has been swapped, there is no difference. It propagates the same as
;; if it were the original component.
(t/is (= (:touched nested2-rectangle) #{:fill-group}))
(t/is (= (:touched copy-nested2-rectangle) nil))
(t/is (= (:touched nested2-rectangle') #{:fill-group}))
(t/is (= (:touched copy-nested2-rectangle') #{:fill-group}))
(t/is (= (count fills') 1))
(t/is (= (:fill-color fill') "#fabada"))
(t/is (= (:fill-opacity fill') 1))))

View File

@@ -17,7 +17,7 @@
(def ^:private defaults
{:public-uri "http://localhost:3449"
:tenant "dev"
:tenant "default"
:host "localhost"
:http-server-port 6061
:http-server-host "0.0.0.0"

View File

@@ -299,19 +299,7 @@
(ptk/reify ::libraries-fetched
ptk/UpdateEvent
(update [_ state]
(let [templates-a-b-test? (cf/external-feature-flag "dashboard-01" "test")
remove-ids (if templates-a-b-test?
#{"wireframing-kit" "prototype-examples" "plants-app" "penpot-design-system"}
#{"prototype-examples" "penpot-design-system"})
libraries (cond->> libraries
:always
(remove #(contains? remove-ids (:id %)))
templates-a-b-test?
(concat [{:id "wireframing-kit", :name "Wireframe library"}
{:id "prototype-examples", :name "Prototype template"}
{:id "plants-app", :name "UI mockup example"}
{:id "penpot-design-system", :name "Design system example"}]))]
(assoc state :builtin-templates libraries)))))
(assoc state :builtin-templates libraries))))
(defn fetch-builtin-templates
[]
@@ -910,8 +898,7 @@
(-> state
(d/update-in-when [:dashboard-files id :is-shared] (constantly is-shared))
(d/update-in-when [:dashboard-recent-files id :is-shared] (constantly is-shared))
(cond->
(not is-shared)
(cond-> (not is-shared)
(d/update-when :dashboard-shared-files dissoc id))))
ptk/WatchEvent
@@ -921,7 +908,7 @@
(rx/ignore))))))
(defn set-file-thumbnail
[file-id thumbnail-uri]
[file-id thumbnail-id]
(ptk/reify ::set-file-thumbnail
ptk/UpdateEvent
(update [_ state]
@@ -929,10 +916,10 @@
(->> files
(mapv #(cond-> %
(= file-id (:id %))
(assoc :thumbnail-uri thumbnail-uri)))))]
(assoc :thumbnail-id thumbnail-id)))))]
(-> state
(d/update-in-when [:dashboard-files file-id] assoc :thumbnail-uri thumbnail-uri)
(d/update-in-when [:dashboard-recent-files file-id] assoc :thumbnail-uri thumbnail-uri)
(d/update-in-when [:dashboard-files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-in-when [:dashboard-recent-files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-when :dashboard-search-result update-search-files))))))
;; --- EVENT: create-file

View File

@@ -12,7 +12,6 @@
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
[app.main.repo :as rp]
[app.util.router :as rt]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@@ -131,8 +130,7 @@
(rx/concat
(if (= :authentication (:type cause))
(rx/empty)
(rx/of (rt/assign-exception cause)
(ptk/data-event ::error cause)
(rx/of (ptk/data-event ::error cause)
(update-status :error)))
(rx/of (discard-persistence-state))
(rx/throw cause))))))))))

View File

@@ -48,7 +48,8 @@
(not= section :auth-register-validate)
(not= section :auth-register-success))
params (:query-params route)
error (:error params)]
error (:error params)
hide-image-auth? (cf/external-feature-flag "signup-01" "test")]
(mf/with-effect []
(dom/set-html-title (tr "title.default")))
@@ -57,14 +58,17 @@
(when error
(st/emit! (du/show-redirect-error error))))
[:main {:class (stl/css :auth-section)}
[:main {:class (stl/css-case :auth-section (not hide-image-auth?)
:auth-section-hide-image hide-image-auth?)}
(when show-login-icon
[:h1 {:class (stl/css :logo-container)}
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]])
[:div {:class (stl/css :login-illustration)}
i/login-illustration]
(when (not hide-image-auth?)
[:div {:class (stl/css :login-illustration)}
i/login-illustration])
[:section {:class (stl/css :auth-content)}
[:section {:class (stl/css-case :auth-content (not hide-image-auth?)
:auth-content-hide-image hide-image-auth?)}
(case section
:auth-register

View File

@@ -24,6 +24,24 @@
}
}
// A-B text signup-01
.auth-section-hide-image {
position: relative;
align-items: center;
background: var(--panel-background-color);
display: grid;
gap: $s-32;
height: 100%;
padding: $s-32;
width: 100%;
overflow: auto;
@media (max-width: 992px) {
display: flex;
justify-content: center;
}
}
.logo-container {
position: absolute;
top: $s-20;
@@ -65,6 +83,19 @@
width: 100%;
}
// A-B text signup-01
.auth-content-hide-image {
display: grid;
grid-template-rows: 1fr auto;
gap: $s-24;
height: fit-content;
margin: auto;
max-width: $s-412;
padding-block-end: $s-8;
position: relative;
width: 100%;
}
.logo-btn {
svg {
width: $s-120;

View File

@@ -59,7 +59,8 @@
:profile-is-muted
(rx/of (msg/error (tr "errors.profile-is-muted")))
:email-has-permanent-bounces
(:email-has-permanent-bounces
:email-has-complaints)
(rx/of (msg/error (tr "errors.email-has-permanent-bounces" (:email data))))
(rx/throw cause)))))

View File

@@ -54,7 +54,7 @@
(defn- on-prepare-register-error
[form cause]
(let [{:keys [type code]} (ex-data cause)]
(let [{:keys [type code] :as edata} (ex-data cause)]
(condp = [type code]
[:restriction :registration-disabled]
(st/emit! (msg/error (tr "errors.registration-disabled")))
@@ -62,6 +62,12 @@
[:restriction :email-domain-is-not-allowed]
(st/emit! (msg/error (tr "errors.email-domain-not-allowed")))
[:restriction :email-has-permanent-bounces]
(st/emit! (msg/error (tr "errors.email-has-permanent-bounces" (:email edata))))
[:restriction :email-has-complaints]
(st/emit! (msg/error (tr "errors.email-has-permanent-bounces" (:email edata))))
[:validation :email-as-password]
(swap! form assoc-in [:errors :password]
{:message "errors.email-as-password"})

View File

@@ -11,6 +11,7 @@
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.logging :as log]
[app.config :as cf]
[app.main.data.dashboard :as dd]
[app.main.data.messages :as msg]
[app.main.features :as features]
@@ -47,7 +48,7 @@
[file-id revn blob]
(let [params {:file-id file-id :revn revn :media blob}]
(->> (rp/cmd! :create-file-thumbnail params)
(rx/map :uri))))
(rx/map :id))))
(defn render-thumbnail
[file-id revn]
@@ -71,15 +72,15 @@
(mf/defc grid-item-thumbnail
{::mf/wrap-props false}
[{:keys [file-id revn thumbnail-uri background-color]}]
[{:keys [file-id revn thumbnail-id background-color]}]
(let [container (mf/use-ref)
visible? (h/use-visible container :once? true)]
(mf/with-effect [file-id revn visible? thumbnail-uri]
(when (and visible? (not thumbnail-uri))
(mf/with-effect [file-id revn visible? thumbnail-id]
(when (and visible? (not thumbnail-id))
(->> (ask-for-thumbnail file-id revn)
(rx/subs! (fn [url]
(st/emit! (dd/set-file-thumbnail file-id url)))
(rx/subs! (fn [thumbnail-id]
(st/emit! (dd/set-file-thumbnail file-id thumbnail-id)))
(fn [cause]
(log/error :hint "unable to render thumbnail"
:file-if file-id
@@ -90,9 +91,9 @@
:style {:background-color background-color}
:ref container}
(when visible?
(if thumbnail-uri
(if thumbnail-id
[:img {:class (stl/css :grid-item-thumbnail-image)
:src thumbnail-uri
:src (cf/resolve-media thumbnail-id)
:loading "lazy"
:decoding "async"}]
i/loader-pencil))]))
@@ -365,7 +366,7 @@
[:& grid-item-thumbnail
{:file-id (:id file)
:revn (:revn file)
:thumbnail-uri (:thumbnail-uri file)
:thumbnail-id (:thumbnail-id file)
:background-color (dm/get-in file [:data :options :background])}])
(when (and (:is-shared file) (not library-view?))

View File

@@ -33,7 +33,6 @@
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(def ^:private arrow-icon
(i/icon-xref :arrow (stl/css :arrow-icon)))
@@ -62,10 +61,10 @@
{::mf/wrap [mf/memo]
::mf/wrap-props false}
[{:keys [section team]}]
(let [on-nav-members (mf/use-fn #(st/emit! (dd/go-to-team-members)))
on-nav-settings (mf/use-fn #(st/emit! (dd/go-to-team-settings)))
on-nav-invitations (mf/use-fn #(st/emit! (dd/go-to-team-invitations)))
on-nav-webhooks (mf/use-fn #(st/emit! (dd/go-to-team-webhooks)))
(let [on-nav-members (mf/use-fn #(st/emit! (dd/go-to-team-members)))
on-nav-settings (mf/use-fn #(st/emit! (dd/go-to-team-settings)))
on-nav-invitations (mf/use-fn #(st/emit! (dd/go-to-team-invitations)))
on-nav-webhooks (mf/use-fn #(st/emit! (dd/go-to-team-webhooks)))
members-section? (= section :dashboard-team-members)
settings-section? (= section :dashboard-team-settings)
@@ -157,21 +156,22 @@
(dd/fetch-team-invitations)))
on-error
(fn [{:keys [type code] :as error}]
(cond
(and (= :validation type)
(= :profile-is-muted code))
(st/emit! (msg/error (tr "errors.profile-is-muted"))
(modal/hide))
(fn [_form cause]
(let [{:keys [type code] :as error} (ex-data cause)]
(cond
(and (= :validation type)
(= :profile-is-muted code))
(st/emit! (msg/error (tr "errors.profile-is-muted"))
(modal/hide))
(and (= :validation type)
(or (= :member-is-muted code)
(= :email-has-permanent-bounces code)))
(swap! error-text (tr "errors.email-spam-or-permanent-bounces" (:email error)))
(or (= :member-is-muted code)
(= :email-has-permanent-bounces code)
(= :email-has-complaints code))
(swap! error-text (tr "errors.email-spam-or-permanent-bounces" (:email error)))
:else
(st/emit! (msg/error (tr "errors.generic"))
(modal/hide))))
:else
(st/emit! (msg/error (tr "errors.generic"))
(modal/hide)))))
on-submit
(fn [form]
@@ -563,22 +563,24 @@
on-error
(mf/use-fn
(mf/deps email)
(fn [{:keys [type code] :as error}]
(cond
(and (= :validation type)
(= :profile-is-muted code))
(rx/of (msg/error (tr "errors.profile-is-muted")))
(fn [cause]
(let [{:keys [type code] :as error} (ex-data cause)]
(cond
(and (= :validation type)
(= :profile-is-muted code))
(rx/of (msg/error (tr "errors.profile-is-muted")))
(and (= :validation type)
(= :member-is-muted code))
(rx/of (msg/error (tr "errors.member-is-muted")))
(and (= :validation type)
(= :member-is-muted code))
(rx/of (msg/error (tr "errors.member-is-muted")))
(and (= :validation type)
(= :email-has-permanent-bounces code))
(rx/of (msg/error (tr "errors.email-has-permanent-bounces" email)))
(and (= :restriction type)
(or (= :email-has-permanent-bounces code)
(= :email-has-complaints code)))
(rx/of (msg/error (tr "errors.email-has-permanent-bounces" email)))
:else
(rx/throw error))))
:else
(rx/throw cause)))))
on-delete
(mf/use-fn
@@ -588,7 +590,6 @@
mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}]
(st/emit! (dd/delete-team-invitation (with-meta params mdata))))))
on-resend-success
(mf/use-fn
(fn []

View File

@@ -70,8 +70,9 @@
(on-update-submit form)
(on-create-submit form))))
(mf/defc team-form-modal {::mf/register modal/components
::mf/register-as :team-form}
(mf/defc team-form-modal
{::mf/register modal/components
::mf/register-as :team-form}
[{:keys [team] :as props}]
(let [initial (mf/use-memo (fn [] (or team {})))
form (fm/use-form :spec ::team-form

View File

@@ -411,9 +411,9 @@
(mf/with-memo []
(-> (shuffle [{:label (tr "labels.youtube") :value "youtube"}
{:label (tr "labels.event") :value "event"}
{:label (tr "labels.search") :value "search"}
{:label (tr "labels.social") :value "social"}
{:label (tr "labels.article") :value "article"}])
{:label (tr "onboarding.questions.referer.search") :value "search"}
{:label (tr "onboarding.questions.referer.social") :value "social"}
{:label (tr "onboarding.questions.referer.article") :value "article"}])
(conj {:label (tr "labels.other-short") :value "other"})))
current-referer

View File

@@ -39,21 +39,22 @@
(s/keys :req-un [::email-1 ::email-2]))
(defn- on-error
[form error]
(case (:code (ex-data error))
:email-already-exists
(swap! form (fn [data]
(let [error {:message (tr "errors.email-already-exists")}]
(assoc-in data [:errors :email-1] error))))
[form cause]
(let [{:keys [code] :as error} (ex-data cause)]
(case code
:email-already-exists
(swap! form (fn [data]
(let [error {:message (tr "errors.email-already-exists")}]
(assoc-in data [:errors :email-1] error))))
:profile-is-muted
(rx/of (msg/error (tr "errors.profile-is-muted")))
:profile-is-muted
(rx/of (msg/error (tr "errors.profile-is-muted")))
:email-has-permanent-bounces
(let [email (get @form [:data :email-1])]
(rx/of (msg/error (tr "errors.email-has-permanent-bounces" email))))
(:email-has-permanent-bounces
:email-has-complaints)
(rx/of (msg/error (tr "errors.email-has-permanent-bounces" (:email error))))
(rx/throw error)))
(rx/throw cause))))
(defn- on-success
[profile data]

View File

@@ -264,8 +264,10 @@
multi-colors? multi-assets? on-asset-click on-assets-delete
on-clear-selection on-group on-rename-group on-ungroup colors
selected-full]}]
(let [group-open? (or ^boolean force-open?
^boolean (get open-groups prefix (if (= prefix "") true false)))
(let [group-open? (if (false? (get open-groups prefix)) ;; if the user has closed it specifically, respect that
false
(or ^boolean force-open?
^boolean (get open-groups prefix (if (= prefix "") true false))))
dragging* (mf/use-state false)
dragging? (deref dragging*)

View File

@@ -128,7 +128,9 @@
[{:keys [file-id prefix groups open-groups force-open? file local? selected local-data
editing-id renaming-id on-asset-click handle-change apply-typography on-rename-group
on-ungroup on-context-menu selected-full]}]
(let [group-open? (get open-groups prefix true)
(let [group-open? (if (false? (get open-groups prefix)) ;; if the user has closed it specifically, respect that
false
(get open-groups prefix true))
dragging* (mf/use-state false)
dragging? (deref dragging*)
selected-paths (mf/with-memo [selected-full]

View File

@@ -2581,15 +2581,15 @@ msgid "labels.event"
msgstr "Event"
#: src/app/main/ui/onboarding/questions.cljs
msgid "labels.search"
msgid "onboarding.questions.referer.search"
msgstr "Search Engine (Google, Yahoo, Bing)"
#: src/app/main/ui/onboarding/questions.cljs
msgid "labels.social"
msgid "onboarding.questions.referer.social"
msgstr "Social Media (X, Linkedin, FB, etc)"
#: src/app/main/ui/onboarding/questions.cljs
msgid "labels.article"
msgid "onboarding.questions.referer.article"
msgstr "Article (Blog, Post, Newsletter)"
#: src/app/main/ui/onboarding/questions.cljs

View File

@@ -1 +1 @@
2.1.0
2.1.2