Compare commits

..

32 Commits

Author SHA1 Message Date
Andrey Antukh
a457f8baf5 Merge pull request #4913 from penpot/superalex-fix-files-with-invalid-library-colors
🐛 Fix file with invalid library colors
2024-07-24 08:37:51 +02:00
Alejandro
3832377e04 Merge pull request #4902 from penpot/niwinz-notifications-improvements
 Improvements to notifications
2024-07-23 16:57:51 +02:00
Andrey Antukh
975efd80cb Add maintenance predefined notification 2024-07-23 16:56:36 +02:00
Alejandro Alonso
ecb0dc073d 🐛 Fix file with invalid library colors 2024-07-23 16:47:23 +02:00
Alejandro
3553b02c55 Merge pull request #4911 from penpot/main-tmp
 Add timestamp to release build hash
2024-07-23 14:49:10 +02:00
Andrey Antukh
434209af7d Add timestamp to release build hash 2024-07-23 14:07:46 +02:00
Alejandro
16ae057b4f Merge pull request #4880 from penpot/niwinz-oidc-fixes
 Several improvements to OIDC and other related code
2024-07-23 11:25:46 +02:00
Alejandro
2431cb40bf Merge pull request #4905 from penpot/niwinz-srepl-improvements
 Add proper deletion/restore helpers to srepl/main
2024-07-23 07:35:55 +02:00
Andrey Antukh
34293326b8 Add proper deletion/restore helpers to srepl/main 2024-07-22 15:17:11 +02:00
Andrey Antukh
57c60716f0 Add minor improvements to notifications-hub component 2024-07-22 13:14:58 +02:00
Andrey Antukh
7e50ab52b9 📎 Add minor improvement on notification message validation 2024-07-22 13:14:58 +02:00
Andrey Antukh
9e0fb44b3f 🐛 Remove unnecesary delay on hide notification banner 2024-07-22 13:14:58 +02:00
Andrey Antukh
142ae32256 Add better call signature for srepl/notify! fn helper 2024-07-22 13:14:58 +02:00
Andrey Antukh
085b933796 Update default buffers and resolver on devenv nginx config 2024-07-12 14:26:21 +02:00
Andrey Antukh
8dfc97d875 Add jwks loading on gitlab oidc provider 2024-07-12 14:03:48 +02:00
Andrey Antukh
3b48be808c 💄 Add minor naming change on calling logging on oidc ns 2024-07-12 13:39:32 +02:00
Andrey Antukh
a54160965d 🐛 Fix ip-addr parsing issue when it comes with port 2024-07-12 13:38:46 +02:00
Andrey Antukh
f4b59cc5a0 Normalize external-session-id parsing from request 2024-07-12 13:38:46 +02:00
Andrey Antukh
d52f2b18a5 Add context to OIDC errors 2024-07-12 13:38:46 +02:00
Eva Marco
e916c97491 Merge pull request #4867 from penpot/ladybenko-8348-fix-pencil-loader
🐛 Fix pencil loader in dashboard
2024-07-11 08:56:04 +02:00
Belén Albeza
cdabf0d6b9 🐛 Fix pencil loader in dashboard 2024-07-11 08:46:16 +02:00
Alejandro
ff43d43020 Merge pull request #4861 from penpot/niwinz-auth-oidc-reject-bugfix
🐛 Fix unexpected error when user explictly reject oidc auth
2024-07-09 14:11:59 +02:00
Andrey Antukh
0ae8cb4979 🐛 Do not report explicit user reject as error on oidc auth process 2024-07-09 13:57:48 +02:00
Andrey Antukh
fc1495fdd1 🐛 Fix unexpected error when user explictly reject oidc auth 2024-07-09 13:51:07 +02:00
Alejandro
74622919f6 Merge pull request #4860 from penpot/palba-testab-template-order
🎉 Test A/B for templates order and names
2024-07-09 12:48:37 +02:00
Pablo Alba
4b4b160ea8 🎉 Test A/B for templates order and names 2024-07-09 12:35:48 +02:00
Alejandro
2baab838e4 Merge pull request #4859 from penpot/hiru-fix-export-include-libs
Fix embed assets in unpublish and export
2024-07-09 12:13:29 +02:00
Andrés Moya
29d0499725 🐛 Fix embed assets in unpublish and export 2024-07-09 11:51:52 +02:00
Andrey Antukh
d99f4f62ea Merge pull request #4858 from penpot/superalex-update-release-name
 Update release 2.1.0 name
2024-07-09 10:22:33 +02:00
Alejandro Alonso
90f545ae6d Update release 2.1.0 name 2024-07-09 10:18:54 +02:00
Andrey Antukh
b295b79565 Merge pull request #4838 from penpot/superalex-fix-size-presets-dont-work-well
🐛 Size presets landscape doesn't work well
2024-07-09 08:26:07 +02:00
Alejandro Alonso
2944860696 🐛 Size presets landscape doesn't work well 2024-07-03 13:35:14 +02:00
26 changed files with 582 additions and 324 deletions

View File

@@ -1,6 +1,12 @@
# CHANGELOG
## 2.1.0
## 2.1.1
### :bug: Bugs fixed
- Fix pencil loader [Taiga #8348](https://tree.taiga.io/project/penpot/issue/8348)
## 2.1.0 - Things can only get better!
### :rocket: Epics and highlights
@@ -54,6 +60,7 @@
- Fix 'Detach instance' shortcut is not working [Taiga #8102](https://tree.taiga.io/project/penpot/issue/8102)
- Fix import file message does not detect 0 as error [Taiga #6824](https://tree.taiga.io/project/penpot/issue/6824)
- Image Color Library is not persisted when exporting/importing in .zip [Taiga #8131](https://tree.taiga.io/project/penpot/issue/8131)
- Fix export files including libraries [Taiga #8266](https://tree.taiga.io/project/penpot/issue/8266)
## 2.0.3

View File

@@ -30,4 +30,10 @@
: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"}]
: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"}]

View File

@@ -19,8 +19,10 @@
[app.email.blacklist :as email.blacklist]
[app.email.whitelist :as email.whitelist]
[app.http.client :as http]
[app.http.errors :as errors]
[app.http.session :as session]
[app.loggers.audit :as audit]
[app.rpc :as rpc]
[app.rpc.commands.profile :as profile]
[app.setup :as-alias setup]
[app.tokens :as tokens]
@@ -130,8 +132,8 @@
(-> body json/decode :keys process-oidc-jwks)
(do
(l/warn :hint "unable to retrieve JWKs (unexpected response status code)"
:http-status status
:http-body body)
:response-status status
:response-body body)
nil)))
(catch Throwable cause
(l/warn :hint "unable to retrieve JWKs (unexpected exception)"
@@ -145,18 +147,18 @@
(when (contains? cf/flags :login-with-oidc)
(if-let [opts (prepare-oidc-opts cfg)]
(let [jwks (fetch-oidc-jwks cfg opts)]
(l/info :hint "provider initialized"
:provider "oidc"
:method (if (:discover? opts) "discover" "manual")
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts))
:scopes (str/join "," (:scopes opts))
:auth-uri (:auth-uri opts)
:user-uri (:user-uri opts)
:token-uri (:token-uri opts)
:roles-attr (:roles-attr opts)
:roles (:roles opts)
:keys (str/join "," (map str (keys jwks))))
(l/inf :hint "provider initialized"
:provider "oidc"
:method (if (:discover? opts) "discover" "manual")
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts))
:scopes (str/join "," (:scopes opts))
:auth-uri (:auth-uri opts)
:user-uri (:user-uri opts)
:token-uri (:token-uri opts)
:roles-attr (:roles-attr opts)
:roles (:roles opts)
:keys (str/join "," (map str (keys jwks))))
(assoc opts :jwks jwks))
(do
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider "oidc")
@@ -180,10 +182,10 @@
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :hint "provider initialized"
:provider "google"
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
(l/inf :hint "provider initialized"
:provider "google"
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
opts)
(do
@@ -208,8 +210,9 @@
(ex/raise :type :internal
:code :unable-to-retrieve-github-emails
:hint "unable to retrieve github emails"
:http-status status
:http-body body))
:request-uri (:uri params)
:response-status status
:response-body body))
(->> body json/decode (filter :primary) first :email))))
@@ -234,10 +237,10 @@
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :hint "provider initialized"
:provider "github"
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
(l/inf :hint "provider initialized"
:provider "github"
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
opts)
(do
@@ -249,7 +252,7 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/init-key ::providers/gitlab
[_ _]
[_ cfg]
(let [base (cf/get :gitlab-base-uri "https://gitlab.com")
opts {:base-uri base
:client-id (cf/get :gitlab-client-id)
@@ -258,17 +261,18 @@
:auth-uri (str base "/oauth/authorize")
:token-uri (str base "/oauth/token")
:user-uri (str base "/oauth/userinfo")
:jwks-uri (str base "/oauth/discovery/keys")
:name "gitlab"}]
(when (contains? cf/flags :login-with-gitlab)
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :hint "provider initialized"
:provider "gitlab"
:base-uri base
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
opts)
(let [jwks (fetch-oidc-jwks cfg opts)]
(l/inf :hint "provider initialized"
:provider "gitlab"
:base-uri base
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
(assoc opts :jwks jwks))
(do
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider "gitlab")
@@ -324,26 +328,31 @@
:uri (:token-uri provider)
:body (u/map->query-string params)}]
(l/trace :hint "request access token"
:provider (:name provider)
:client-id (:client-id provider)
:client-secret (obfuscate-string (:client-secret provider))
:grant-type (:grant_type params)
:redirect-uri (:redirect_uri params))
(l/trc :hint "fetch access token"
:provider (:name provider)
:client-id (:client-id provider)
:client-secret (obfuscate-string (:client-secret provider))
:grant-type (:grant_type params)
:redirect-uri (:redirect_uri params))
(let [{:keys [status body]} (http/req! cfg req {:sync? true})]
(l/trace :hint "access token response" :status status :body body)
(l/trc :hint "access token fetched" :status status :body body)
(if (= status 200)
(let [data (json/decode body)]
{:token/access (get data :access_token)
:token/id (get data :id_token)
:token/type (get data :token_type)})
(let [data (json/decode body)
data {:token/access (get data :access_token)
:token/id (get data :id_token)
:token/type (get data :token_type)}]
(l/trc :hint "access token fetched"
:token-id (:token/id data)
:token-type (:token/type data)
:token (:token/access data))
data)
(ex/raise :type :internal
:code :unable-to-retrieve-token
:hint "unable to retrieve token"
:http-status status
:http-body body)))))
:code :unable-to-fetch-access-token
:hint "unable to fetch access token"
:request-uri (:uri req)
:response-status status
:response-body body)))))
(defn- process-user-info
[provider tdata info]
@@ -370,9 +379,9 @@
(defn- fetch-user-info
[{:keys [::provider] :as cfg} tdata]
(l/trace :hint "fetch user info"
:uri (:user-uri provider)
:token (obfuscate-string (:token/access tdata)))
(l/trc :hint "fetch user info"
:uri (:user-uri provider)
:token (obfuscate-string (:token/access tdata)))
(let [params {:uri (:user-uri provider)
:headers {"Authorization" (str (:token/type tdata) " " (:token/access tdata))}
@@ -380,9 +389,9 @@
:method :get}
response (http/req! cfg params {:sync? true})]
(l/trace :hint "user info response"
:status (:status response)
:body (:body response))
(l/trc :hint "user info response"
:status (:status response)
:body (:body response))
(when-not (s/int-in-range? 200 300 (:status response))
(ex/raise :type :internal
@@ -420,12 +429,6 @@
(defn- get-info
[{:keys [::provider ::setup/props] :as cfg} {:keys [params] :as request}]
(when-let [error (get params :error)]
(ex/raise :type :internal
:code :error-on-retrieving-code
:error-id error
:error-desc (get params :error_description)))
(let [state (get params :state)
code (get params :code)
state (tokens/verify props {:token state :iss :oauth})
@@ -438,7 +441,7 @@
info (process-user-info provider tdata info)]
(l/trace :hint "user info" :info info)
(l/trc :hint "user info" :info info)
(when-not (s/valid? ::info info)
(l/warn :hint "received incomplete profile info object (please set correct scopes)" :info info)
@@ -592,28 +595,50 @@
(redirect-to-register cfg info request)
(redirect-with-error "registration-disabled")))))
(defn- get-external-session-id
[request]
(let [session-id (rreq/get-header request "x-external-session-id")]
(when (string? session-id)
(if (or (> (count session-id) 256)
(= session-id "null")
(str/blank? session-id))
nil
session-id))))
(defn- auth-handler
[cfg {:keys [params] :as request}]
(let [props (audit/extract-utm-params params)
esid (rreq/get-header request "x-external-session-id")
state (tokens/generate (::setup/props cfg)
{:iss :oauth
:invitation-token (:invitation-token params)
:external-session-id esid
:props props
:exp (dt/in-future "4h")})
uri (build-auth-uri cfg state)]
(let [props (audit/extract-utm-params params)
esid (rpc/get-external-session-id request)
params {:iss :oauth
:invitation-token (:invitation-token params)
:external-session-id esid
:props props
:exp (dt/in-future "4h")}
state (tokens/generate (::setup/props cfg)
(d/without-nils params))
uri (build-auth-uri cfg state)]
{::rres/status 200
::rres/body {:redirect-uri uri}}))
(defn- callback-handler
[cfg request]
[{:keys [::provider] :as cfg} request]
(try
(let [info (get-info cfg request)
profile (get-profile cfg info)]
(process-callback cfg request info profile))
(if-let [error (dm/get-in request [:params :error])]
(redirect-with-error "unable-to-auth" error)
(let [info (get-info cfg request)
profile (get-profile cfg info)]
(process-callback cfg request info profile)))
(catch Throwable cause
(l/err :hint "error on oauth process" :cause cause)
(binding [l/*context* (-> (errors/request->context request)
(assoc :auth/provider (:name provider)))]
(let [edata (ex-data cause)]
(cond
(= :validation (:type edata))
(l/wrn :hint "invalid token received" :cause cause)
:else
(l/err :hint "error on oauth process" :cause cause))))
(redirect-with-error "unable-to-auth" (ex-message cause)))))
(def provider-lookup

View File

@@ -15,6 +15,7 @@
[app.common.files.migrations :as fmg]
[app.common.files.validate :as fval]
[app.common.logging :as l]
[app.common.types.file :as ctf]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
@@ -331,54 +332,12 @@
(defn embed-assets
[cfg data file-id]
(letfn [(walk-map-form [form state]
(cond
(uuid? (:fill-color-ref-file form))
(do
(vswap! state conj [(:fill-color-ref-file form) :colors (:fill-color-ref-id form)])
(assoc form :fill-color-ref-file file-id))
(uuid? (:stroke-color-ref-file form))
(do
(vswap! state conj [(:stroke-color-ref-file form) :colors (:stroke-color-ref-id form)])
(assoc form :stroke-color-ref-file file-id))
(uuid? (:typography-ref-file form))
(do
(vswap! state conj [(:typography-ref-file form) :typographies (:typography-ref-id form)])
(assoc form :typography-ref-file file-id))
(uuid? (:component-file form))
(do
(vswap! state conj [(:component-file form) :components (:component-id form)])
(assoc form :component-file file-id))
:else
form))
(process-group-of-assets [data [lib-id items]]
;; NOTE: there is a possibility that shape refers to an
;; non-existant file because the file was removed. In this
;; case we just ignore the asset.
(if-let [lib (get-file cfg lib-id)]
(reduce (partial process-asset lib) data items)
data))
(process-asset [lib data [bucket asset-id]]
(let [asset (get-in lib [:data bucket asset-id])
;; Add a special case for colors that need to have
;; correctly set the :file-id prop (pending of the
;; refactor that will remove it).
asset (cond-> asset
(= bucket :colors) (assoc :file-id file-id))]
(update data bucket assoc asset-id asset)))]
(let [assets (volatile! [])]
(walk/postwalk #(cond-> % (map? %) (walk-map-form assets)) data)
(->> (deref assets)
(filter #(as-> (first %) $ (and (uuid? $) (not= $ file-id))))
(d/group-by first rest)
(reduce (partial process-group-of-assets) data)))))
(let [library-ids (get-libraries cfg [file-id])]
(reduce (fn [data library-id]
(let [library (get-file cfg library-id)]
(ctf/absorb-assets data (:data library))))
data
library-ids)))
(defn- fix-version
[file]

View File

@@ -130,7 +130,6 @@
(.writeLong output (long data))
(swap! *position* + 8))
(defn read-long!
[^DataInputStream input]
(let [v (.readLong input)]

View File

@@ -35,9 +35,13 @@
(defn parse-client-ip
[request]
(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)))
(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
@@ -192,15 +196,33 @@
(::webhooks/event? resultm)
false)}))
(defn- handle-event!
[cfg event]
(defn- event->params
[event]
(let [params {:id (uuid/next)
:name (::name event)
:type (::type event)
:profile-id (::profile-id event)
:ip-addr (::ip-addr event)
:context (::context event)
:props (::props event)}
:ip-addr (::ip-addr event "0.0.0.0")
:context (::context event {})
:props (::props event {})
:source "backend"}
tnow (::tracked-at event)]
(cond-> params
(some? tnow)
(assoc :tracked-at tnow))))
(defn- append-audit-entry!
[cfg params]
(let [params (-> params
(update :props db/tjson)
(update :context db/tjson)
(update :ip-addr db/inet))]
(db/insert! cfg :audit-log params)))
(defn- handle-event!
[cfg event]
(let [params (event->params event)
tnow (dt/now)]
(when (contains? cf/flags :audit-log)
@@ -209,12 +231,8 @@
;; this case we just retry the operation.
(let [params (-> params
(assoc :created-at tnow)
(assoc :tracked-at tnow)
(update :props db/tjson)
(update :context db/tjson)
(update :ip-addr db/inet)
(assoc :source "backend"))]
(db/insert! cfg :audit-log params)))
(update :tracked-at #(or % tnow)))]
(append-audit-entry! cfg params)))
(when (and (or (contains? cf/flags :telemetry)
(cf/get :telemetry-enabled))
@@ -226,12 +244,11 @@
;; NOTE: this is only executed when general audit log is disabled
(let [params (-> params
(assoc :created-at tnow)
(assoc :tracked-at tnow)
(assoc :props (db/tjson {}))
(assoc :context (db/tjson {}))
(assoc :ip-addr (db/inet "0.0.0.0"))
(assoc :source "backend"))]
(db/insert! cfg :audit-log params)))
(update :tracked-at #(or % tnow))
(assoc :props {})
(assoc :context {})
(assoc :ip-addr "0.0.0.0"))]
(append-audit-entry! cfg params)))
(when (and (contains? cf/flags :webhooks)
(::webhooks/event? event))
@@ -258,9 +275,9 @@
(defn submit!
"Submit audit event to the collector."
[cfg params]
[cfg event]
(try
(let [event (d/without-nils params)
(let [event (d/without-nils event)
cfg (-> cfg
(assoc ::rtry/when rtry/conflict-exception?)
(assoc ::rtry/max-retries 6)
@@ -269,3 +286,18 @@
(rtry/invoke! cfg db/tx-run! handle-event! event))
(catch Throwable cause
(l/error :hint "unexpected error processing event" :cause cause))))
(defn insert!
"Submit audit event to the collector, intended to be used only from
command line helpers because this skips all webhooks and telemetry
logic."
[cfg event]
(when (contains? cf/flags :audit-log)
(let [event (d/without-nils event)]
(us/verify! ::event event)
(db/run! cfg (fn [cfg]
(let [tnow (dt/now)
params (-> (event->params event)
(assoc :created-at tnow)
(update :tracked-at #(or % tnow)))]
(append-audit-entry! cfg params)))))))

View File

@@ -254,7 +254,7 @@
{::http.client/client (ig/ref ::http.client/client)}
::oidc.providers/gitlab
{}
{::http.client/client (ig/ref ::http.client/client)}
::oidc.providers/generic
{::http.client/client (ig/ref ::http.client/client)}

View File

@@ -70,6 +70,20 @@
(handle-response-transformation request mdata)
(handle-before-comple-hook mdata))))
(defn get-external-session-id
[request]
(when-let [session-id (rreq/get-header request "x-external-session-id")]
(when-not (or (> (count session-id) 256)
(= session-id "null")
(str/blank? session-id))
session-id)))
(defn- get-external-event-origin
[request]
(when-let [origin (rreq/get-header request "x-event-origin")]
(when-not (> (count origin) 256)
origin)))
(defn- rpc-handler
"Ring handler that dispatches cmd requests and convert between
internal async flow into ring async flow."
@@ -79,8 +93,8 @@
profile-id (or (::session/profile-id request)
(::actoken/profile-id request))
session-id (rreq/get-header request "x-external-session-id")
event-origin (rreq/get-header request "x-event-origin")
session-id (get-external-session-id request)
event-origin (get-external-event-origin request)
data (-> params
(assoc ::handler-name handler-name)

View File

@@ -21,8 +21,10 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as-alias sql]
[app.features.components-v2 :as feat.comp-v2]
[app.features.fdata :as feat.fdata]
[app.loggers.audit :as audit]
[app.main :as main]
[app.msgbus :as mbus]
[app.rpc.commands.auth :as auth]
@@ -38,10 +40,12 @@
[app.util.pointer-map :as pmap]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.java.io :as io]
[clojure.pprint :refer [print-table]]
[clojure.stacktrace :as strace]
[clojure.tools.namespace.repl :as repl]
[cuerdas.core :as str]
[datoteka.fs :as fs]
[promesa.exec :as px]
[promesa.exec.semaphore :as ps]
[promesa.util :as pu]))
@@ -190,6 +194,12 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn notify!
"Send flash notifications.
This method allows send flash notifications to specified target destinations.
The message can be a free text or a preconfigured one.
The destination can be: all, profile-id, team-id, or a coll of them."
[{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level]
:or {code :generic level :info}
:as params}]
@@ -197,10 +207,6 @@
["invalid level %" level]
(contains? #{:success :error :info :warning} level))
(dm/verify!
["invalid code: %" code]
(contains? #{:generic :upgrade-version} code))
(letfn [(send [dest]
(l/inf :hint "sending notification" :dest (str dest))
(let [message {:type :notification
@@ -226,6 +232,9 @@
(resolve-dest [dest]
(cond
(= :all dest)
[uuid/zero]
(uuid? dest)
[dest]
@@ -241,14 +250,15 @@
(mapcat resolve-dest))
dest)
(and (coll? dest)
(every? coll? dest))
(and (vector? dest)
(every? vector? dest))
(sequence (comp
(map vec)
(mapcat resolve-dest))
dest)
(vector? dest)
(and (vector? dest)
(keyword? (first dest)))
(let [[op param] dest]
(cond
(= op :email)
@@ -475,6 +485,27 @@
;; DELETE/RESTORE OBJECTS (WITH CASCADE, SOFT)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn delete-file!
"Mark a project for deletion"
[file-id]
(let [file-id (h/parse-uuid file-id)
tnow (dt/now)]
(audit/insert! main/system
{::audit/name "delete-file"
::audit/type "action"
::audit/profile-id uuid/zero
::audit/props {:id file-id}
::audit/context {:triggered-by "srepl"
:cause "explicit call to delete-file!"}
::audit/tracked-at tnow})
(wrk/invoke! (-> main/system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :file
:deleted-at tnow
:id file-id})))
:deleted))
(defn- restore-file*
[{:keys [::db/conn]} file-id]
(db/update! conn :file
@@ -502,20 +533,105 @@
:restored)
(defn restore-file!
"Mark a file and all related objects as not deleted"
[file-id]
(let [file-id (h/parse-uuid file-id)]
(db/tx-run! main/system
(fn [system]
(when-let [file (some-> (db/get* system :file
{:id file-id}
{::db/remove-deleted false
::sql/columns [:id :name]})
(files/decode-row))]
(audit/insert! system
{::audit/name "restore-file"
::audit/type "action"
::audit/profile-id uuid/zero
::audit/props file
::audit/context {:triggered-by "srepl"
:cause "explicit call to restore-file!"}
::audit/tracked-at (dt/now)})
(restore-file* system file-id))))))
(defn delete-project!
"Mark a project for deletion"
[project-id]
(let [project-id (h/parse-uuid project-id)
tnow (dt/now)]
(audit/insert! main/system
{::audit/name "delete-project"
::audit/type "action"
::audit/profile-id uuid/zero
::audit/props {:id project-id}
::audit/context {:triggered-by "srepl"
:cause "explicit call to delete-project!"}
::audit/tracked-at tnow})
(wrk/invoke! (-> main/system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :project
:deleted-at tnow
:id project-id})))
:deleted))
(defn- restore-project*
[{:keys [::db/conn] :as cfg} project-id]
(db/update! conn :project
{:deleted-at nil}
{:id project-id})
(doseq [{:keys [id]} (db/query conn :file
{:project-id project-id}
{::db/columns [:id]})]
{::sql/columns [:id]})]
(restore-file* cfg id))
:restored)
(defn restore-project!
"Mark a project and all related objects as not deleted"
[project-id]
(let [project-id (h/parse-uuid project-id)]
(db/tx-run! main/system
(fn [system]
(when-let [project (db/get* system :project
{:id project-id}
{::db/remove-deleted false})]
(audit/insert! system
{::audit/name "restore-project"
::audit/type "action"
::audit/profile-id uuid/zero
::audit/props project
::audit/context {:triggered-by "srepl"
:cause "explicit call to restore-team!"}
::audit/tracked-at (dt/now)})
(restore-project* system project-id))))))
(defn delete-team!
"Mark a team for deletion"
[team-id]
(let [team-id (h/parse-uuid team-id)
tnow (dt/now)]
(audit/insert! main/system
{::audit/name "delete-team"
::audit/type "action"
::audit/profile-id uuid/zero
::audit/props {:id team-id}
::audit/context {:triggered-by "srepl"
:cause "explicit call to delete-profile!"}
::audit/tracked-at tnow})
(wrk/invoke! (-> main/system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :team
:deleted-at tnow
:id team-id})))
:deleted))
(defn- restore-team*
[{:keys [::db/conn] :as cfg} team-id]
(db/update! conn :team
@@ -528,84 +644,127 @@
(doseq [{:keys [id]} (db/query conn :project
{:team-id team-id}
{::db/columns [:id]})]
{::sql/columns [:id]})]
(restore-project* cfg id))
:restored)
(defn- restore-profile*
[{:keys [::db/conn] :as cfg} profile-id]
(db/update! conn :profile
{:deleted-at nil}
{:id profile-id})
(doseq [{:keys [id]} (profile/get-owned-teams conn profile-id)]
(restore-team* cfg id))
:restored)
(defn restore-deleted-profile!
"Mark a team and all related objects as not deleted"
[profile-id]
(let [profile-id (h/parse-uuid profile-id)]
(db/tx-run! main/system restore-profile* profile-id)))
(defn restore-deleted-team!
(defn restore-team!
"Mark a team and all related objects as not deleted"
[team-id]
(let [team-id (h/parse-uuid team-id)]
(db/tx-run! main/system restore-team* team-id)))
(db/tx-run! main/system
(fn [system]
(when-let [team (some-> (db/get* system :team
{:id team-id}
{::db/remove-deleted false})
(teams/decode-row))]
(audit/insert! system
{::audit/name "restore-team"
::audit/type "action"
::audit/profile-id uuid/zero
::audit/props team
::audit/context {:triggered-by "srepl"
:cause "explicit call to restore-team!"}
::audit/tracked-at (dt/now)})
(defn restore-deleted-project!
"Mark a project and all related objects as not deleted"
[project-id]
(let [project-id (h/parse-uuid project-id)]
(db/tx-run! main/system restore-project* project-id)))
(restore-team* system team-id))))))
(defn restore-deleted-file!
"Mark a file and all related objects as not deleted"
[file-id]
(let [file-id (h/parse-uuid file-id)]
(db/tx-run! main/system restore-file* file-id)))
(defn delete-team!
"Mark a team for deletion"
[team-id]
(let [team-id (h/parse-uuid team-id)]
(wrk/invoke! (-> main/system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :team
:deleted-at (dt/now)
:id team-id})))))
(defn delete-profile!
"Mark a profile for deletion"
"Mark a profile for deletion."
[profile-id]
(let [profile-id (h/parse-uuid profile-id)]
(let [profile-id (h/parse-uuid profile-id)
tnow (dt/now)]
(audit/insert! main/system
{::audit/name "delete-profile"
::audit/type "action"
::audit/profile-id uuid/zero
::audit/context {:triggered-by "srepl"
:cause "explicit call to delete-profile!"}
::audit/tracked-at tnow})
(wrk/invoke! (-> main/system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :profile
:deleted-at (dt/now)
:id profile-id})))))
(defn delete-project!
"Mark a project for deletion"
[project-id]
(let [project-id (h/parse-uuid project-id)]
(wrk/invoke! (-> main/system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :project
:deleted-at (dt/now)
:id project-id})))))
:deleted-at tnow
:id profile-id})))
:deleted))
(defn delete-file!
"Mark a project for deletion"
[file-id]
(let [file-id (h/parse-uuid file-id)]
(wrk/invoke! (-> main/system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :file
:deleted-at (dt/now)
:id file-id})))))
(defn restore-profile!
"Mark a team and all related objects as not deleted"
[profile-id]
(let [profile-id (h/parse-uuid profile-id)]
(db/tx-run! main/system
(fn [system]
(when-let [profile (some-> (db/get* system :profile
{:id profile-id}
{::db/remove-deleted false})
(profile/decode-row))]
(audit/insert! system
{::audit/name "restore-profile"
::audit/type "action"
::audit/profile-id uuid/zero
::audit/props (audit/profile->props profile)
::audit/context {:triggered-by "srepl"
:cause "explicit call to restore-profile!"}
::audit/tracked-at (dt/now)})
(db/update! system :profile
{:deleted-at nil}
{:id profile-id}
{::db/return-keys false})
(doseq [{:keys [id]} (profile/get-owned-teams system profile-id)]
(restore-team* system id))
:restored)))))
(defn delete-profiles-in-bulk!
[system path]
(letfn [(process-data! [system deleted-at emails]
(loop [emails emails
deleted 0
total 0]
(if-let [email (first emails)]
(if-let [profile (db/get* system :profile
{:email (str/lower email)}
{::db/remove-deleted false})]
(do
(audit/insert! system
{::audit/name "delete-profile"
::audit/type "action"
::audit/tracked-at deleted-at
::audit/props (audit/profile->props profile)
::audit/context {:triggered-by "srepl"
:cause "explicit call to delete-profiles-in-bulk!"}})
(wrk/invoke! (-> system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :profile
:deleted-at deleted-at
:id (:id profile)})))
(recur (rest emails)
(inc deleted)
(inc total)))
(recur (rest emails)
deleted
(inc total)))
{:deleted deleted :total total})))]
(let [path (fs/path path)
deleted-at (dt/minus (dt/now) cf/deletion-delay)]
(when-not (fs/exists? path)
(throw (ex-info "path does not exists" {:path path})))
(db/tx-run! system
(fn [system]
(with-open [reader (io/reader path)]
(process-data! system deleted-at (line-seq reader))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CASCADE FIXING
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn process-deleted-profiles-cascade
[]

View File

@@ -6,4 +6,4 @@
(ns app.common.files.defaults)
(def version 50)
(def version 51)

View File

@@ -22,6 +22,7 @@
[app.common.schema :as sm]
[app.common.svg :as csvg]
[app.common.text :as txt]
[app.common.types.color :as ctc]
[app.common.types.component :as ctk]
[app.common.types.file :as ctf]
[app.common.types.shape :as cts]
@@ -1004,6 +1005,17 @@
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(def ^:private valid-color?
(sm/lazy-validator ::ctc/color))
(defn migrate-up-51
"This migration fixes library invalid colors"
[data]
(let [update-colors
(fn [colors]
(into {} (filter #(-> % val valid-color?) colors)))]
(update data :colors update-colors)))
(def migrations
"A vector of all applicable migrations"
@@ -1046,4 +1058,5 @@
{:id 47 :migrate-up migrate-up-47}
{:id 48 :migrate-up migrate-up-48}
{:id 49 :migrate-up migrate-up-49}
{:id 50 :migrate-up migrate-up-50}])
{:id 50 :migrate-up migrate-up-50}
{:id 51 :migrate-up migrate-up-51}])

View File

@@ -633,19 +633,24 @@
"Find all assets of a library that are used in the file, and
move them to the file local library."
[file-data library-data]
(let [used-components (find-asset-type-usages file-data library-data :component)
used-colors (find-asset-type-usages file-data library-data :color)
used-typographies (find-asset-type-usages file-data library-data :typography)]
(let [used-components (find-asset-type-usages file-data library-data :component)
file-data (cond-> file-data
(d/not-empty? used-components)
(absorb-components used-components library-data))
;; Note that absorbed components may also be using colors
;; and typographies. This is the reason of doing this first
;; and accumulating file data for the next ones.
(cond-> file-data
(d/not-empty? used-components)
(absorb-components used-components library-data)
used-colors (find-asset-type-usages file-data library-data :color)
file-data (cond-> file-data
(d/not-empty? used-colors)
(absorb-colors used-colors))
(d/not-empty? used-colors)
(absorb-colors used-colors)
(d/not-empty? used-typographies)
(absorb-typographies used-typographies))))
used-typographies (find-asset-type-usages file-data library-data :typography)
file-data (cond-> file-data
(d/not-empty? used-typographies)
(absorb-typographies used-typographies))]
file-data))
;; Debug helpers

View File

@@ -68,7 +68,10 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
resolver 127.0.0.11;
proxy_buffer_size 16k;
proxy_busy_buffers_size 24k; # essentially, proxy_buffer_size + 2 small buffers of 4k
proxy_buffers 32 4k;
resolver 127.0.0.11 ipv6=off;
etag off;

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

View File

@@ -8,6 +8,7 @@ export CURRENT_VERSION=$1;
export BUILD_DATE=$(date -R);
export CURRENT_HASH=${CURRENT_HASH:-$(git rev-parse --short HEAD)};
export EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS;
export TS=$(date +%s);
# Some cljs reacts on this environment variable for define more
# performant code on macros (example: rumext)
@@ -17,7 +18,7 @@ yarn install || exit 1;
rm -rf resources/public;
rm -rf target/dist;
clojure -M:dev:shadow-cljs release main --config-merge "{:release-version \"${CURRENT_HASH}\"}" $EXTRA_PARAMS || exit 1
clojure -M:dev:shadow-cljs release main --config-merge "{:release-version \"${CURRENT_HASH}-${TS}\"}" $EXTRA_PARAMS || exit 1
yarn run compile || exit 1;
mkdir -p target/dist;

View File

@@ -13,6 +13,7 @@
[app.main.data.modal :as modal]
[app.main.features :as features]
[app.main.repo :as rp]
[app.main.store :as st]
[app.util.i18n :refer [tr]]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@@ -58,6 +59,10 @@
[]
(.reload js/location))
(defn hide-notifications!
[]
(st/emit! msg/hide))
(defn handle-notification
[{:keys [message code level] :as params}]
(ptk/reify ::show-notification
@@ -75,6 +80,15 @@
:actions [{:label "Refresh" :callback force-reload!}]
:tag :notification)))
:maintenance
(rx/of (msg/dialog
:content (tr "notifications.by-code.maintenance")
:controls :inline-actions
:type level
:actions [{:label (tr "labels.accept")
:callback hide-notifications!}]
:tag :notification))
(rx/of (msg/dialog
:content message
:controls :close

View File

@@ -299,7 +299,19 @@
(ptk/reify ::libraries-fetched
ptk/UpdateEvent
(update [_ state]
(assoc state :builtin-templates libraries))))
(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)))))
(defn fetch-builtin-templates
[]

View File

@@ -15,42 +15,42 @@
(declare hide)
(declare show)
(def default-animation-timeout 600)
(def default-timeout 7000)
(def ^:private
schema:message
(sm/define
[:map {:title "Message"}
[:type [::sm/one-of #{:success :error :info :warning}]]
[:status {:optional true}
[::sm/one-of #{:visible :hide}]]
[:position {:optional true}
[::sm/one-of #{:fixed :floating :inline}]]
[:notification-type {:optional true}
[::sm/one-of #{:inline :context :toast}]]
[:controls {:optional true}
[::sm/one-of #{:none :close :inline-actions :bottom-actions}]]
[:tag {:optional true}
[:or :string :keyword]]
[:timeout {:optional true}
[:maybe :int]]
[:actions {:optional true}
[:vector
[:map
[:label :string]
[:callback ::sm/fn]]]]
[:links {:optional true}
[:vector
[:map
[:label :string]
[:callback ::sm/fn]]]]]))
(def ^:private schema:message
[:map {:title "Message"}
[:type [::sm/one-of #{:success :error :info :warning}]]
[:status {:optional true}
[::sm/one-of #{:visible :hide}]]
[:position {:optional true}
[::sm/one-of #{:fixed :floating :inline}]]
[:notification-type {:optional true}
[::sm/one-of #{:inline :context :toast}]]
[:controls {:optional true}
[::sm/one-of #{:none :close :inline-actions :bottom-actions}]]
[:tag {:optional true}
[:or :string :keyword]]
[:timeout {:optional true}
[:maybe :int]]
[:actions {:optional true}
[:vector
[:map
[:label :string]
[:callback ::sm/fn]]]]
[:links {:optional true}
[:vector
[:map
[:label :string]
[:callback ::sm/fn]]]]])
(def ^:private valid-message?
(sm/validator schema:message))
(defn show
[data]
(dm/assert!
"expected valid message map"
(sm/check! schema:message data))
(valid-message? data))
(ptk/reify ::show
ptk/UpdateEvent
@@ -76,14 +76,7 @@
(ptk/reify ::hide
ptk/UpdateEvent
(update [_ state]
(d/update-when state :message assoc :status :hide))
ptk/WatchEvent
(watch [_ _ stream]
(let [stopper (rx/filter (ptk/type? ::show) stream)]
(->> (rx/of #(dissoc % :message))
(rx/delay default-animation-timeout)
(rx/take-until stopper))))))
(dissoc state :message))))
(defn hide-tag
[tag]

View File

@@ -696,15 +696,20 @@
(ptk/reify ::show-redirect-error
ptk/WatchEvent
(watch [_ _ _]
(let [hint (case error
"registration-disabled"
(tr "errors.registration-disabled")
"profile-blocked"
(tr "errors.profile-blocked")
"auth-provider-not-allowed"
(tr "errors.auth-provider-not-allowed")
"email-domain-not-allowed"
(tr "errors.email-domain-not-allowed")
:else
(tr "errors.generic"))]
(when-let [hint (case error
"registration-disabled"
(tr "errors.registration-disabled")
"profile-blocked"
(tr "errors.profile-blocked")
"auth-provider-not-allowed"
(tr "errors.auth-provider-not-allowed")
"email-domain-not-allowed"
(tr "errors.email-domain-not-allowed")
;; We explicitly do not show any error here, it a explicit user operation.
"unable-to-auth"
nil
(tr "errors.generic"))]
(rx/of (msg/warn hint))))))

View File

@@ -58,7 +58,7 @@
(mf/defc radio-buttons
{::mf/props :obj}
[{:keys [children on-change selected class wide encode-fn decode-fn allow-empty] :as props}]
[{:keys [name children on-change selected class wide encode-fn decode-fn allow-empty] :as props}]
(let [encode-fn (d/nilv encode-fn identity)
decode-fn (d/nilv decode-fn identity)
nitems (if (array? children)
@@ -94,5 +94,6 @@
[:& (mf/provider context) {:value context-value}
[:div {:class (dm/str class " " (stl/css :radio-btn-wrapper))
:style {:width width}}
:style {:width width}
:key (dm/str name "-" selected)}
children]]))

View File

@@ -41,5 +41,5 @@
(mf/defc loading-placeholder
[]
[:div {:class (stl/css :grid-empty-placeholder :loader)}
[:div {:class (stl/css :icon)} i/loader]
[:div {:class (stl/css :icon)} i/loader-pencil]
[:div {:class (stl/css :text)} (tr "dashboard.loading-files")]])

View File

@@ -23,8 +23,7 @@
svg {
width: $s-64;
height: $s-64;
stroke: $df-secondary;
fill: none;
fill: $df-secondary;
}
}

View File

@@ -17,33 +17,38 @@
(mf/defc notifications-hub
[]
(let [message (mf/deref refs/message)
on-close #(st/emit! dmsg/hide)
toast-message {:type (or (:type message) :info)
:links (:links message)
:on-close on-close
:content (:content message)}
inline-message {:actions (:actions message)
:links (:links message)
:content (:content message)}
context-message {:type (or (:type message) :info)
:links (:links message)
:content (:content message)}
is-context-msg (and (nil? (:timeout message)) (nil? (:actions message)))
is-toast-msg (or (= :toast (:notification-type message)) (some? (:timeout message)))
is-inline-msg (or (= :inline (:notification-type message)) (and (some? (:position message)) (= :floating (:position message))))]
on-close (mf/use-fn #(st/emit! dmsg/hide))
context? (and (nil? (:timeout message))
(nil? (:actions message)))
inline? (or (= :inline (:notification-type message))
(= :floating (:position message)))
toast? (or (= :toast (:notification-type message))
(some? (:timeout message)))]
(when message
(cond
is-toast-msg
[:& toast-notification toast-message]
is-inline-msg
[:& inline-notification inline-message]
is-context-msg
[:& context-notification context-message]
toast?
[:& toast-notification
{:type (or (:type message) :info)
:links (:links message)
:on-close on-close
:content (:content message)}]
inline?
[:& inline-notification
{:actions (:actions message)
:links (:links message)
:content (:content message)}]
context?
[:& context-notification
{:type (or (:type message) :info)
:links (:links message)
:content (:content message)}]
:else
[:& toast-notification toast-message]))))
[:& toast-notification
{:type (or (:type message) :info)
:links (:links message)
:on-close on-close
:content (:content message)}]))))

View File

@@ -38,12 +38,10 @@
neutral-icon))
(mf/defc toast-notification
"These are ephemeral elements that disappear when
the close button is pressed,
the page is refreshed,
the page is navigated to another page or
after 7 seconds, which is enough time to be read,
except for error messages that require user interaction."
"These are ephemeral elements that disappear when the close button
is pressed, the page is refreshed, the page is navigated to another
page or after 7 seconds, which is enough time to be read, except for
error messages that require user interaction."
{::mf/props :obj}
[{:keys [type content on-close links] :as props}]

View File

@@ -2209,6 +2209,10 @@ msgstr "Update a component in a shared library"
msgid "notifications.by-code.upgrade-version"
msgstr "A new version is available, please refresh the page"
#: src/app/main/data/common.cljs
msgid "notifications.by-code.maintenance"
msgstr "Maintenance break: we will be down for a short maintenance within 5 minutes."
#: src/app/main/ui/dashboard/team.cljs
msgid "notifications.invitation-email-sent"
msgstr "Invitation sent successfully"

View File

@@ -2285,6 +2285,10 @@ msgstr "Actualizar un componente en biblioteca"
msgid "notifications.by-code.upgrade-version"
msgstr "Una nueva versión está disponible, por favor actualiza la página"
#: src/app/main/data/common.cljs
msgid "notifications.by-code.maintenance"
msgstr "Pausa de mantenimiento: en los próximos 5 minutos estaremos fuera de servicio por un breve mantenimiento."
#: src/app/main/ui/dashboard/team.cljs
msgid "notifications.invitation-email-sent"
msgstr "Invitación enviada con éxito"