mirror of
https://github.com/penpot/penpot.git
synced 2026-09-10 14:09:17 +00:00
🎉 Add link preview metadata for shared links
This commit is contained in:
parent
e67ebacef2
commit
e052a7befc
22
backend/resources/app/templates/unfurl.tmpl
Normal file
22
backend/resources/app/templates/unfurl.tmpl
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>{{title}}</title>
|
||||||
|
<meta name="robots" content="noindex" />
|
||||||
|
<meta name="description" content="{{description}}" />
|
||||||
|
<meta property="og:site_name" content="Penpot" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:locale" content="en_US" />
|
||||||
|
<meta property="og:title" content="{{title}}" />
|
||||||
|
<meta property="og:description" content="{{description}}" />
|
||||||
|
<meta property="og:image" content="{{image}}" />
|
||||||
|
<meta name="twitter:title" content="{{title}}" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:description" content="{{description}}" />
|
||||||
|
<meta name="twitter:image" content="{{image}}" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>location.replace("/" + location.hash);</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -21,6 +21,7 @@
|
|||||||
[app.http.middleware :as mw]
|
[app.http.middleware :as mw]
|
||||||
[app.http.security :as sec]
|
[app.http.security :as sec]
|
||||||
[app.http.session :as session]
|
[app.http.session :as session]
|
||||||
|
[app.http.unfurl :as-alias unfurl]
|
||||||
[app.http.websocket :as-alias ws]
|
[app.http.websocket :as-alias ws]
|
||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
@ -149,6 +150,7 @@
|
|||||||
[::rpc/routes schema:routes]
|
[::rpc/routes schema:routes]
|
||||||
[::oidc/routes schema:routes]
|
[::oidc/routes schema:routes]
|
||||||
[::assets/routes schema:routes]
|
[::assets/routes schema:routes]
|
||||||
|
[::unfurl/routes schema:routes]
|
||||||
[::debug/routes schema:routes]
|
[::debug/routes schema:routes]
|
||||||
[::mtx/routes schema:routes]
|
[::mtx/routes schema:routes]
|
||||||
[::awsns/routes schema:routes]
|
[::awsns/routes schema:routes]
|
||||||
@ -177,6 +179,7 @@
|
|||||||
|
|
||||||
(::mtx/routes cfg)
|
(::mtx/routes cfg)
|
||||||
(::assets/routes cfg)
|
(::assets/routes cfg)
|
||||||
|
(::unfurl/routes cfg)
|
||||||
(::debug/routes cfg)
|
(::debug/routes cfg)
|
||||||
|
|
||||||
["/webhooks"
|
["/webhooks"
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
[app.common.uri :as u]
|
[app.common.uri :as u]
|
||||||
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.http.access-token :as actoken]
|
[app.http.access-token :as actoken]
|
||||||
[app.http.session :as session]
|
[app.http.session :as session]
|
||||||
@ -97,11 +98,19 @@
|
|||||||
(:s3 :assets-s3) (serve-object-from-s3 cfg obj)
|
(:s3 :assets-s3) (serve-object-from-s3 cfg obj)
|
||||||
(:fs :assets-fs) (serve-object-from-fs cfg obj)))
|
(:fs :assets-fs) (serve-object-from-fs cfg obj)))
|
||||||
|
|
||||||
|
(defn- public-bucket?
|
||||||
|
[bucket]
|
||||||
|
(or (contains? public-buckets bucket)
|
||||||
|
;; Dashboard file thumbnails become public when link unfurling
|
||||||
|
;; is enabled, so link preview crawlers can fetch them.
|
||||||
|
(and (= "file-thumbnail" bucket)
|
||||||
|
(contains? cf/flags :link-unfurl))))
|
||||||
|
|
||||||
(defn- requires-auth?
|
(defn- requires-auth?
|
||||||
"Check if the storage object requires authentication based on its bucket."
|
"Check if the storage object requires authentication based on its bucket."
|
||||||
[obj]
|
[obj]
|
||||||
(let [bucket (-> obj meta :bucket)]
|
(let [bucket (-> obj meta :bucket)]
|
||||||
(not (contains? public-buckets bucket))))
|
(not (public-bucket? bucket))))
|
||||||
|
|
||||||
(defn- request-profile-id
|
(defn- request-profile-id
|
||||||
"Extract the authenticated profile-id from the request."
|
"Extract the authenticated profile-id from the request."
|
||||||
|
|||||||
85
backend/src/app/http/unfurl.clj
Normal file
85
backend/src/app/http/unfurl.clj
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
;; 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 Sucursal en España SL
|
||||||
|
|
||||||
|
(ns app.http.unfurl
|
||||||
|
"Link unfurl (Open Graph metadata) related handlers.
|
||||||
|
|
||||||
|
Serves a minimal HTML page with Open Graph metadata used by link
|
||||||
|
preview crawlers (Slack, Discord, Twitter, ...). The reverse proxy
|
||||||
|
routes crawler requests for the application root to this endpoint,
|
||||||
|
preserving the query string params that the frontend mirrors on
|
||||||
|
navigation (`file-id`, `project-id` and `team-id`)."
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.util.template :as tmpl]
|
||||||
|
[clojure.java.io :as io]
|
||||||
|
[integrant.core :as ig]
|
||||||
|
[yetti.response :as-alias yres]))
|
||||||
|
|
||||||
|
(def ^:private default-context
|
||||||
|
{:title "Penpot | Full-stack design"
|
||||||
|
:description "Penpot is the open-source design platform for teams that build digital products at scale."})
|
||||||
|
|
||||||
|
(def ^:private sql:get-file
|
||||||
|
"SELECT f.name, ft.media_id
|
||||||
|
FROM file AS f
|
||||||
|
LEFT JOIN file_thumbnail AS ft
|
||||||
|
ON (ft.file_id = f.id AND ft.deleted_at IS NULL)
|
||||||
|
WHERE f.id = ?
|
||||||
|
AND f.deleted_at IS NULL
|
||||||
|
ORDER BY ft.revn DESC NULLS LAST
|
||||||
|
LIMIT 1")
|
||||||
|
|
||||||
|
(defn- resolve-image-uri
|
||||||
|
[media-id]
|
||||||
|
(str (cf/get :public-uri) "/assets/by-id/" media-id))
|
||||||
|
|
||||||
|
(defn- resolve-default-image-uri
|
||||||
|
[]
|
||||||
|
(str (cf/get :public-uri) "/images/penpot-link-preview.png"))
|
||||||
|
|
||||||
|
(defn- get-file-context
|
||||||
|
"Return the unfurl context for a file link: the file name as title
|
||||||
|
and, when available, the last dashboard thumbnail as image."
|
||||||
|
[pool file-id]
|
||||||
|
(when-let [{:keys [name media-id]} (db/exec-one! pool [sql:get-file file-id])]
|
||||||
|
(cond-> (assoc default-context :title (str name " | Penpot"))
|
||||||
|
(some? media-id)
|
||||||
|
(assoc :image (resolve-image-uri media-id)))))
|
||||||
|
|
||||||
|
(defn- get-context
|
||||||
|
[pool params]
|
||||||
|
(let [file-id (some-> (:file-id params) d/parse-uuid)
|
||||||
|
project-id (some-> (:project-id params) d/parse-uuid)
|
||||||
|
team-id (some-> (:team-id params) d/parse-uuid)]
|
||||||
|
(cond
|
||||||
|
(some? file-id) (get-file-context pool file-id)
|
||||||
|
(some? project-id) (assoc default-context :title "Project | Penpot")
|
||||||
|
(some? team-id) (assoc default-context :title "Team dashboard | Penpot"))))
|
||||||
|
|
||||||
|
(defn- handler
|
||||||
|
[{:keys [::db/pool]} request]
|
||||||
|
(let [context (when (contains? cf/flags :link-unfurl)
|
||||||
|
(get-context pool (:query-params request)))
|
||||||
|
context (-> (or context default-context)
|
||||||
|
(update :image #(or % (resolve-default-image-uri))))]
|
||||||
|
{::yres/status 200
|
||||||
|
::yres/headers {"content-type" "text/html; charset=utf-8"
|
||||||
|
"cache-control" "no-store, no-cache, max-age=0"}
|
||||||
|
::yres/body (-> (io/resource "app/templates/unfurl.tmpl")
|
||||||
|
(tmpl/render context))}))
|
||||||
|
|
||||||
|
;; --- Initialization
|
||||||
|
|
||||||
|
(defmethod ig/assert-key ::routes
|
||||||
|
[_ params]
|
||||||
|
(assert (db/pool? (::db/pool params)) "expect valid database pool"))
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::routes
|
||||||
|
[_ cfg]
|
||||||
|
["/unfurl" {:handler (partial handler cfg)}])
|
||||||
@ -23,6 +23,7 @@
|
|||||||
[app.http.management :as mgmt]
|
[app.http.management :as mgmt]
|
||||||
[app.http.session :as session]
|
[app.http.session :as session]
|
||||||
[app.http.session.tasks :as-alias session.tasks]
|
[app.http.session.tasks :as-alias session.tasks]
|
||||||
|
[app.http.unfurl :as-alias http.unfurl]
|
||||||
[app.http.websocket :as http.ws]
|
[app.http.websocket :as http.ws]
|
||||||
[app.loggers.webhooks :as-alias webhooks]
|
[app.loggers.webhooks :as-alias webhooks]
|
||||||
[app.metrics :as-alias mtx]
|
[app.metrics :as-alias mtx]
|
||||||
@ -283,9 +284,13 @@
|
|||||||
::mgmt/routes (ig/ref ::mgmt/routes)
|
::mgmt/routes (ig/ref ::mgmt/routes)
|
||||||
::http.debug/routes (ig/ref ::http.debug/routes)
|
::http.debug/routes (ig/ref ::http.debug/routes)
|
||||||
::http.assets/routes (ig/ref ::http.assets/routes)
|
::http.assets/routes (ig/ref ::http.assets/routes)
|
||||||
|
::http.unfurl/routes (ig/ref ::http.unfurl/routes)
|
||||||
::http.ws/routes (ig/ref ::http.ws/routes)
|
::http.ws/routes (ig/ref ::http.ws/routes)
|
||||||
::http.awsns/routes (ig/ref ::http.awsns/routes)}
|
::http.awsns/routes (ig/ref ::http.awsns/routes)}
|
||||||
|
|
||||||
|
::http.unfurl/routes
|
||||||
|
{::db/pool (ig/ref ::db/pool)}
|
||||||
|
|
||||||
::http.debug/routes
|
::http.debug/routes
|
||||||
{::db/pool (ig/ref ::db/pool)
|
{::db/pool (ig/ref ::db/pool)
|
||||||
::rds/pool (ig/ref ::rds/pool)
|
::rds/pool (ig/ref ::rds/pool)
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.http :as-alias http]
|
[app.http :as-alias http]
|
||||||
[app.http.access-token :as actoken]
|
[app.http.access-token :as actoken]
|
||||||
@ -157,6 +158,26 @@
|
|||||||
;; Tests: objects-handler — non-public buckets (auth required)
|
;; Tests: objects-handler — non-public buckets (auth required)
|
||||||
;; ----------------------------------------------------------------
|
;; ----------------------------------------------------------------
|
||||||
|
|
||||||
|
(t/deftest objects-handler-file-thumbnail-bucket-link-unfurl-flag
|
||||||
|
;; Objects in the file-thumbnail bucket are public only when the
|
||||||
|
;; link-unfurl flag is enabled.
|
||||||
|
(let [storage (-> (:app.storage/storage th/*system*)
|
||||||
|
(configure-storage-backend))
|
||||||
|
cfg (make-handler-cfg storage)
|
||||||
|
object (create-storage-object! storage "file-thumbnail" "thumbnail data")
|
||||||
|
request {:path-params {:id (str (:id object))}}]
|
||||||
|
|
||||||
|
(t/testing "flag enabled"
|
||||||
|
(with-redefs [cf/flags (conj cf/flags :link-unfurl)]
|
||||||
|
(let [response (assets/objects-handler cfg request)]
|
||||||
|
(t/is (not= 401 (::yres/status response)))
|
||||||
|
(t/is (not= 404 (::yres/status response))))))
|
||||||
|
|
||||||
|
(t/testing "flag disabled"
|
||||||
|
(with-redefs [cf/flags (disj cf/flags :link-unfurl)]
|
||||||
|
(let [response (assets/objects-handler cfg request)]
|
||||||
|
(t/is (= 401 (::yres/status response))))))))
|
||||||
|
|
||||||
(t/deftest objects-handler-non-public-bucket-no-auth
|
(t/deftest objects-handler-non-public-bucket-no-auth
|
||||||
;; Objects in non-public buckets should return 401 without authentication.
|
;; Objects in non-public buckets should return 401 without authentication.
|
||||||
(let [storage (-> (:app.storage/storage th/*system*)
|
(let [storage (-> (:app.storage/storage th/*system*)
|
||||||
@ -218,10 +239,12 @@
|
|||||||
cfg (make-handler-cfg storage)
|
cfg (make-handler-cfg storage)
|
||||||
profile (th/create-profile* 1)]
|
profile (th/create-profile* 1)]
|
||||||
|
|
||||||
|
;; NOTE: file-thumbnail is not included here because it is public
|
||||||
|
;; when the link-unfurl flag is enabled; see
|
||||||
|
;; objects-handler-file-thumbnail-bucket-link-unfurl-flag.
|
||||||
(doseq [bucket ["profile"
|
(doseq [bucket ["profile"
|
||||||
"tempfile"
|
"tempfile"
|
||||||
"file-data"
|
"file-data"
|
||||||
"file-thumbnail"
|
|
||||||
"file-change"]]
|
"file-change"]]
|
||||||
(t/testing (str "bucket: " bucket)
|
(t/testing (str "bucket: " bucket)
|
||||||
(let [object (create-storage-object! storage bucket "some data")
|
(let [object (create-storage-object! storage bucket "some data")
|
||||||
|
|||||||
98
backend/test/backend_tests/http_unfurl_test.clj
Normal file
98
backend/test/backend_tests/http_unfurl_test.clj
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
;; 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 Sucursal en España SL
|
||||||
|
|
||||||
|
(ns backend-tests.http-unfurl-test
|
||||||
|
(:require
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.http.unfurl :as unfurl]
|
||||||
|
[app.storage :as sto]
|
||||||
|
[backend-tests.helpers :as th]
|
||||||
|
[clojure.test :as t]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[yetti.response :as-alias yres]))
|
||||||
|
|
||||||
|
(t/use-fixtures :once th/state-init)
|
||||||
|
(t/use-fixtures :each (th/serial
|
||||||
|
th/database-reset
|
||||||
|
th/clean-storage))
|
||||||
|
|
||||||
|
(def ^:private default-title
|
||||||
|
"Penpot | Full-stack design")
|
||||||
|
|
||||||
|
(defn- run-handler
|
||||||
|
[query-params]
|
||||||
|
(let [cfg {::db/pool (:app.db/pool th/*system*)}]
|
||||||
|
(#'unfurl/handler cfg {:query-params query-params})))
|
||||||
|
|
||||||
|
(defn- create-file-thumbnail!
|
||||||
|
[file-id]
|
||||||
|
(let [storage (::sto/storage th/*system*)
|
||||||
|
object (sto/put-object! storage {::sto/content (sto/content "thumbnail data")
|
||||||
|
:bucket "file-thumbnail"
|
||||||
|
:content-type "image/png"})]
|
||||||
|
(db/insert! (:app.db/pool th/*system*) :file-thumbnail
|
||||||
|
{:file-id file-id
|
||||||
|
:revn 1
|
||||||
|
:media-id (:id object)})
|
||||||
|
object))
|
||||||
|
|
||||||
|
(t/deftest unfurl-without-params
|
||||||
|
(let [response (run-handler {})]
|
||||||
|
(t/is (= 200 (::yres/status response)))
|
||||||
|
(t/is (str/includes? (::yres/body response) default-title))
|
||||||
|
(t/is (str/includes? (::yres/body response) "/images/penpot-link-preview.png"))))
|
||||||
|
|
||||||
|
(t/deftest unfurl-file-without-thumbnail
|
||||||
|
(let [profile (th/create-profile* 1)
|
||||||
|
file (th/create-file* 1 {:profile-id (:id profile)
|
||||||
|
:project-id (:default-project-id profile)})
|
||||||
|
response (run-handler {:file-id (str (:id file))})]
|
||||||
|
(t/is (= 200 (::yres/status response)))
|
||||||
|
(t/is (str/includes? (::yres/body response) (str (:name file) " | Penpot")))
|
||||||
|
(t/is (str/includes? (::yres/body response) "/images/penpot-link-preview.png"))))
|
||||||
|
|
||||||
|
(t/deftest unfurl-file-with-thumbnail
|
||||||
|
(let [profile (th/create-profile* 1)
|
||||||
|
file (th/create-file* 1 {:profile-id (:id profile)
|
||||||
|
:project-id (:default-project-id profile)})
|
||||||
|
object (create-file-thumbnail! (:id file))
|
||||||
|
response (run-handler {:file-id (str (:id file))})]
|
||||||
|
(t/is (= 200 (::yres/status response)))
|
||||||
|
(t/is (str/includes? (::yres/body response) (str (:name file) " | Penpot")))
|
||||||
|
(t/is (str/includes? (::yres/body response) (str "/assets/by-id/" (:id object))))))
|
||||||
|
|
||||||
|
(t/deftest unfurl-non-existent-file
|
||||||
|
(let [response (run-handler {:file-id (str (uuid/next))})]
|
||||||
|
(t/is (= 200 (::yres/status response)))
|
||||||
|
(t/is (str/includes? (::yres/body response) default-title))))
|
||||||
|
|
||||||
|
(t/deftest unfurl-invalid-file-id
|
||||||
|
(let [response (run-handler {:file-id "not-a-uuid"})]
|
||||||
|
(t/is (= 200 (::yres/status response)))
|
||||||
|
(t/is (str/includes? (::yres/body response) default-title))))
|
||||||
|
|
||||||
|
(t/deftest unfurl-team-link
|
||||||
|
(let [response (run-handler {:team-id (str (uuid/next))})]
|
||||||
|
(t/is (= 200 (::yres/status response)))
|
||||||
|
(t/is (str/includes? (::yres/body response) "Team dashboard | Penpot"))))
|
||||||
|
|
||||||
|
(t/deftest unfurl-project-link
|
||||||
|
(let [response (run-handler {:team-id (str (uuid/next))
|
||||||
|
:project-id (str (uuid/next))})]
|
||||||
|
(t/is (= 200 (::yres/status response)))
|
||||||
|
(t/is (str/includes? (::yres/body response) "Project | Penpot"))))
|
||||||
|
|
||||||
|
(t/deftest unfurl-flag-disabled
|
||||||
|
(let [profile (th/create-profile* 1)
|
||||||
|
file (th/create-file* 1 {:profile-id (:id profile)
|
||||||
|
:project-id (:default-project-id profile)})]
|
||||||
|
(with-redefs [cf/flags (disj cf/flags :link-unfurl)]
|
||||||
|
(let [response (run-handler {:file-id (str (:id file))})]
|
||||||
|
(t/is (= 200 (::yres/status response)))
|
||||||
|
(t/is (str/includes? (::yres/body response) default-title))
|
||||||
|
(t/is (not (str/includes? (::yres/body response) (:name file))))))))
|
||||||
@ -185,7 +185,12 @@
|
|||||||
;; renderer.
|
;; renderer.
|
||||||
:wasm-export
|
:wasm-export
|
||||||
:custom-shortcuts
|
:custom-shortcuts
|
||||||
:remote-media-processing})
|
:remote-media-processing
|
||||||
|
|
||||||
|
;; Enables serving link preview (Open Graph) metadata for shared
|
||||||
|
;; links; exposes file names and dashboard thumbnails to anyone
|
||||||
|
;; that knows the file id.
|
||||||
|
:link-unfurl})
|
||||||
|
|
||||||
(def all-flags
|
(def all-flags
|
||||||
(set/union email login varia))
|
(set/union email login varia))
|
||||||
@ -220,7 +225,8 @@
|
|||||||
:enable-available-viewer-wasm
|
:enable-available-viewer-wasm
|
||||||
:enable-background-blur
|
:enable-background-blur
|
||||||
:enable-stroke-path
|
:enable-stroke-path
|
||||||
:enable-token-combobox])
|
:enable-token-combobox
|
||||||
|
:enable-link-unfurl])
|
||||||
|
|
||||||
(defn parse
|
(defn parse
|
||||||
[& flags]
|
[& flags]
|
||||||
|
|||||||
@ -49,6 +49,13 @@ http {
|
|||||||
'' close;
|
'' close;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Link preview crawlers; their requests for the application root
|
||||||
|
# are served with dynamic Open Graph metadata from the backend.
|
||||||
|
map $http_user_agent $penpot_unfurl_agent {
|
||||||
|
default 0;
|
||||||
|
~*(slackbot|discordbot|twitterbot|facebookexternalhit|facebookcatalog|whatsapp|telegrambot|linkedinbot|skypeuripreview|pinterestbot|redditbot|embedly|iframely|mastodon|bluesky) 1;
|
||||||
|
}
|
||||||
|
|
||||||
proxy_cache_path /tmp/cache/ levels=2:2 keys_zone=penpot:20m;
|
proxy_cache_path /tmp/cache/ levels=2:2 keys_zone=penpot:20m;
|
||||||
proxy_cache_methods GET HEAD;
|
proxy_cache_methods GET HEAD;
|
||||||
proxy_cache_valid any 48h;
|
proxy_cache_valid any 48h;
|
||||||
@ -116,6 +123,10 @@ http {
|
|||||||
add_header x-internal-redirect "$upstream_http_x_accel_redirect";
|
add_header x-internal-redirect "$upstream_http_x_accel_redirect";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location = /unfurl {
|
||||||
|
proxy_pass http://127.0.0.1:6060/unfurl$is_args$args;
|
||||||
|
}
|
||||||
|
|
||||||
# On production, this is controlled by ELB
|
# On production, this is controlled by ELB
|
||||||
location /api/export {
|
location /api/export {
|
||||||
proxy_pass http://127.0.0.1:6061;
|
proxy_pass http://127.0.0.1:6061;
|
||||||
@ -292,6 +303,10 @@ http {
|
|||||||
return 301 " /404";
|
return 301 " /404";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($penpot_unfurl_agent) {
|
||||||
|
rewrite ^/$ /unfurl last;
|
||||||
|
}
|
||||||
|
|
||||||
include /home/penpot/penpot/docker/devenv/files/nginx-security-headers.conf;
|
include /home/penpot/penpot/docker/devenv/files/nginx-security-headers.conf;
|
||||||
add_header Cache-Control "no-store" always;
|
add_header Cache-Control "no-store" always;
|
||||||
try_files $uri /index.html$is_args$args /index.html =404;
|
try_files $uri /index.html$is_args$args /index.html =404;
|
||||||
|
|||||||
@ -57,6 +57,13 @@ http {
|
|||||||
'' close;
|
'' close;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Link preview crawlers; their requests for the application root
|
||||||
|
# are served with dynamic Open Graph metadata from the backend.
|
||||||
|
map $http_user_agent $penpot_unfurl_agent {
|
||||||
|
default 0;
|
||||||
|
~*(slackbot|discordbot|twitterbot|facebookexternalhit|facebookcatalog|whatsapp|telegrambot|linkedinbot|skypeuripreview|pinterestbot|redditbot|embedly|iframely|mastodon|bluesky) 1;
|
||||||
|
}
|
||||||
|
|
||||||
proxy_cache_path /tmp/cache/ levels=2:2 keys_zone=penpot:20m;
|
proxy_cache_path /tmp/cache/ levels=2:2 keys_zone=penpot:20m;
|
||||||
proxy_cache_methods GET HEAD;
|
proxy_cache_methods GET HEAD;
|
||||||
proxy_cache_valid any 48h;
|
proxy_cache_valid any 48h;
|
||||||
@ -127,6 +134,10 @@ http {
|
|||||||
add_header x-internal-redirect "$upstream_http_x_accel_redirect";
|
add_header x-internal-redirect "$upstream_http_x_accel_redirect";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location = /unfurl {
|
||||||
|
proxy_pass $PENPOT_BACKEND_URI/unfurl$is_args$args;
|
||||||
|
}
|
||||||
|
|
||||||
location /api/export {
|
location /api/export {
|
||||||
proxy_set_header Host $proxy_host;
|
proxy_set_header Host $proxy_host;
|
||||||
proxy_pass $PENPOT_EXPORTER_URI;
|
proxy_pass $PENPOT_EXPORTER_URI;
|
||||||
@ -180,6 +191,10 @@ http {
|
|||||||
return 301 " /404";
|
return 301 " /404";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($penpot_unfurl_agent) {
|
||||||
|
rewrite ^/$ /unfurl last;
|
||||||
|
}
|
||||||
|
|
||||||
include /etc/nginx/nginx-security-headers.conf;
|
include /etc/nginx/nginx-security-headers.conf;
|
||||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||||
try_files $uri /index.html$is_args$args /index.html =404;
|
try_files $uri /index.html$is_args$args /index.html =404;
|
||||||
|
|||||||
@ -65,6 +65,23 @@
|
|||||||
|
|
||||||
;; --- Navigate (Event)
|
;; --- Navigate (Event)
|
||||||
|
|
||||||
|
(defn match->context-params
|
||||||
|
"Extract the params that give sharing context to the current URL.
|
||||||
|
|
||||||
|
They are mirrored on the query string (before the fragment) because
|
||||||
|
the fragment is never sent to the server; this way shared links
|
||||||
|
carry enough context for rendering link preview (unfurl) metadata."
|
||||||
|
[match]
|
||||||
|
(let [path-params (dm/get-in match [:params :path])
|
||||||
|
query-params (get match :query-params)
|
||||||
|
file-id (or (get query-params :file-id) (get path-params :file-id))
|
||||||
|
team-id (or (get query-params :team-id) (get path-params :team-id))
|
||||||
|
project-id (or (get query-params :project-id) (get path-params :project-id))]
|
||||||
|
(cond
|
||||||
|
(some? file-id) {:file-id file-id}
|
||||||
|
(some? project-id) {:team-id team-id :project-id project-id}
|
||||||
|
(some? team-id) {:team-id team-id})))
|
||||||
|
|
||||||
(defn navigated
|
(defn navigated
|
||||||
[match send-event-info?]
|
[match send-event-info?]
|
||||||
(ptk/reify ::navigated
|
(ptk/reify ::navigated
|
||||||
@ -85,7 +102,16 @@
|
|||||||
(update [_ state]
|
(update [_ state]
|
||||||
(-> state
|
(-> state
|
||||||
(assoc :route match)
|
(assoc :route match)
|
||||||
(dissoc :exception)))))
|
(dissoc :exception)))
|
||||||
|
|
||||||
|
ptk/EffectEvent
|
||||||
|
(effect [_ _ _]
|
||||||
|
(let [query (some-> (match->context-params match)
|
||||||
|
(u/map->query-string))
|
||||||
|
href (dm/str (.-pathname globals/location)
|
||||||
|
(if (some? query) (dm/str "?" query) "")
|
||||||
|
(.-hash globals/location))]
|
||||||
|
(.replaceState js/history nil "" href)))))
|
||||||
|
|
||||||
(defn navigate
|
(defn navigate
|
||||||
[id params & {:keys [::replace ::new-window] :as options}]
|
[id params & {:keys [::replace ::new-window] :as options}]
|
||||||
|
|||||||
41
frontend/test/frontend_tests/router_test.cljs
Normal file
41
frontend/test/frontend_tests/router_test.cljs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
;; 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 Sucursal en España SL
|
||||||
|
|
||||||
|
(ns frontend-tests.router-test
|
||||||
|
(:require
|
||||||
|
[app.main.router :as rt]
|
||||||
|
[cljs.test :as t :include-macros true]))
|
||||||
|
|
||||||
|
(t/deftest match-context-params-file-link
|
||||||
|
;; Workspace and viewer links only mirror the file-id.
|
||||||
|
(let [match {:query-params {:team-id "team-1"
|
||||||
|
:file-id "file-1"
|
||||||
|
:page-id "page-1"}}]
|
||||||
|
(t/is (= {:file-id "file-1"}
|
||||||
|
(rt/match->context-params match)))))
|
||||||
|
|
||||||
|
(t/deftest match-context-params-file-link-path-params
|
||||||
|
;; Legacy routes carry the ids as path params.
|
||||||
|
(let [match {:params {:path {:project-id "project-1"
|
||||||
|
:file-id "file-1"}}}]
|
||||||
|
(t/is (= {:file-id "file-1"}
|
||||||
|
(rt/match->context-params match)))))
|
||||||
|
|
||||||
|
(t/deftest match-context-params-project-link
|
||||||
|
(let [match {:query-params {:team-id "team-1"
|
||||||
|
:project-id "project-1"}}]
|
||||||
|
(t/is (= {:team-id "team-1"
|
||||||
|
:project-id "project-1"}
|
||||||
|
(rt/match->context-params match)))))
|
||||||
|
|
||||||
|
(t/deftest match-context-params-team-link
|
||||||
|
(let [match {:query-params {:team-id "team-1"}}]
|
||||||
|
(t/is (= {:team-id "team-1"}
|
||||||
|
(rt/match->context-params match)))))
|
||||||
|
|
||||||
|
(t/deftest match-context-params-no-context
|
||||||
|
(let [match {:query-params {:token "some-token"}}]
|
||||||
|
(t/is (nil? (rt/match->context-params match)))))
|
||||||
@ -68,6 +68,7 @@
|
|||||||
[frontend-tests.render-wasm.process-objects-test]
|
[frontend-tests.render-wasm.process-objects-test]
|
||||||
[frontend-tests.render-wasm.text-editor-apply-styles-test]
|
[frontend-tests.render-wasm.text-editor-apply-styles-test]
|
||||||
[frontend-tests.render-wasm.text-editor-caret-color-test]
|
[frontend-tests.render-wasm.text-editor-caret-color-test]
|
||||||
|
[frontend-tests.router-test]
|
||||||
[frontend-tests.svg-fills-test]
|
[frontend-tests.svg-fills-test]
|
||||||
[frontend-tests.text-editor-paste-guard-test]
|
[frontend-tests.text-editor-paste-guard-test]
|
||||||
[frontend-tests.tokens.copy-paste-props-test]
|
[frontend-tests.tokens.copy-paste-props-test]
|
||||||
@ -175,6 +176,7 @@
|
|||||||
'frontend-tests.render-wasm.process-objects-test
|
'frontend-tests.render-wasm.process-objects-test
|
||||||
'frontend-tests.render-wasm.text-editor-apply-styles-test
|
'frontend-tests.render-wasm.text-editor-apply-styles-test
|
||||||
'frontend-tests.render-wasm.text-editor-caret-color-test
|
'frontend-tests.render-wasm.text-editor-caret-color-test
|
||||||
|
'frontend-tests.router-test
|
||||||
'frontend-tests.svg-fills-test
|
'frontend-tests.svg-fills-test
|
||||||
'frontend-tests.tokens.copy-paste-props-test
|
'frontend-tests.tokens.copy-paste-props-test
|
||||||
'frontend-tests.tokens.import-export-test
|
'frontend-tests.tokens.import-export-test
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user