Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
.rebel_readline_history
.repl
.shadow-cljs
.pnpm-store/
/*.jpg
/*.md
/*.png
Expand Down Expand Up @@ -71,6 +72,7 @@
/library/target/
/library/*.zip
/external
/penpot-nitrate

clj-profiler/
node_modules
Expand Down
5 changes: 4 additions & 1 deletion backend/scripts/_env
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export PENPOT_FLAGS="\
enable-file-validation \
enable-file-schema-validation \
enable-redis-cache \
enable-subscriptions";
enable-subscriptions \
enable-nitrate";

# Default deletion delay for devenv
export PENPOT_DELETION_DELAY="24h"
Expand All @@ -55,6 +56,8 @@ export PENPOT_OBJECTS_STORAGE_BACKEND=s3
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot

export PENPOT_NITRATE_BACKEND_URI=http://localhost:3000

export JAVA_OPTS="\
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-Djdk.attach.allowAttachSelf \
Expand Down
2 changes: 2 additions & 0 deletions backend/src/app/config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@
[:netty-io-threads {:optional true} ::sm/int]
[:executor-threads {:optional true} ::sm/int]

[:nitrate-backend-uri {:optional true} ::sm/uri]

;; DEPRECATED
[:assets-storage-backend {:optional true} :keyword]
[:storage-assets-fs-directory {:optional true} :string]
Expand Down
5 changes: 5 additions & 0 deletions backend/src/app/main.clj
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@
{::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)
::rds/pool (ig/ref ::rds/pool)
:app.nitrate/client (ig/ref :app.nitrate/client)
::wrk/executor (ig/ref ::wrk/netty-executor)
::session/manager (ig/ref ::session/manager)
::ldap/provider (ig/ref ::ldap/provider)
Expand All @@ -339,6 +340,9 @@
::email/blacklist (ig/ref ::email/blacklist)
::email/whitelist (ig/ref ::email/whitelist)}

:app.nitrate/client
{::http.client/client (ig/ref ::http.client/client)}

:app.rpc/management-methods
{::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)
Expand All @@ -348,6 +352,7 @@
::sto/storage (ig/ref ::sto/storage)
::mtx/metrics (ig/ref ::mtx/metrics)
::mbus/msgbus (ig/ref ::mbus/msgbus)
:app.nitrate/client (ig/ref :app.nitrate/client)
::rds/client (ig/ref ::rds/client)
::setup/props (ig/ref ::setup/props)}

Expand Down
123 changes: 123 additions & 0 deletions backend/src/app/nitrate.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
;; 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.nitrate
"Module that make calls to the external nitrate aplication"
(:require
[app.common.logging :as l]
[app.common.schema :as sm]
[app.config :as cf]
[app.http.client :as http]
[app.rpc :as-alias rpc]
[app.setup :as-alias setup]
[app.util.json :as json]
[clojure.core :as c]
[integrant.core :as ig]))


(def baseuri (cf/get :nitrate-backend-uri))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn- coercer
[schema & {:as opts}]
(let [decode-fn (sm/decoder schema sm/json-transformer)
check-fn (sm/check-fn schema opts)]
(fn [data]
(-> data decode-fn check-fn))))


(defn- request-builder
[cfg method uri management-key profile-id]
(fn []
(http/req! cfg {:method method
:headers {"content-type" "application/json"
"accept" "application/json"
"x-shared-key" management-key
"x-profile-id" (str profile-id)}
:uri uri
:version :http1.1})))


(defn- with-retries
[handler max-retries]
(fn []
(loop [attempt 1]
(let [result (try
(handler)
(catch Exception e
(if (< attempt max-retries)
::retry
(do
;; TODO Error handling
(l/error :hint "request fail after multiple retries" :cause e)
nil))))]
(if (= result ::retry)
(recur (inc attempt))
result)))))


(defn- with-validate [handler uri schema]
(fn []
(let [coercer-http (coercer schema
:type :validation
:hint (str "invalid data received calling " uri))]
(try
(coercer-http (-> (handler) :body json/decode))
(catch Exception e
;; TODO Error handling
(l/error :hint "error validating json response" :cause e)
nil)))))

(defn- request-to-nitrate
[{:keys [::management-key] :as cfg} method uri schema {:keys [::rpc/profile-id] :as params}]
(let [full-http-call (-> (request-builder cfg method uri management-key profile-id)
(with-retries 3)
(with-validate uri schema))]
(full-http-call)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn call
[cfg method params]
(when (contains? cf/flags :nitrate)
(let [client (get cfg ::client)
method (get client method)]
(method params))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def ^:private schema:organization
[:map
[:id ::sm/int]
[:name ::sm/text]])


(defn- get-team-org
[cfg {:keys [team-id] :as params}]
(request-to-nitrate cfg :get (str baseuri "/api/teams/" (str team-id)) schema:organization params))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INITIALIZATION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defmethod ig/init-key ::client
[_ {:keys [::setup/props] :as cfg}]
(if (contains? cf/flags :nitrate)
(let [management-key (or (cf/get :management-api-key)
(get props :management-key))
cfg (assoc cfg ::management-key management-key)]
{:get-team-org (partial get-team-org cfg)})
{}))

(defmethod ig/halt-key! ::client
[_ {:keys []}]
(do :stuff))
1 change: 1 addition & 0 deletions backend/src/app/rpc.clj
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@
(let [cfg (assoc cfg ::type "management" ::metrics-id :rpc-management-timing)]
(->> (sv/scan-ns
'app.rpc.management.subscription
'app.rpc.management.nitrate
'app.rpc.management.exporter)
(map (partial process-method cfg "management" wrap-management))
(into {}))))
Expand Down
11 changes: 10 additions & 1 deletion backend/src/app/rpc/commands/teams.clj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
[app.main :as-alias main]
[app.media :as media]
[app.msgbus :as mbus]
[app.nitrate :as nitrate]
[app.rpc :as-alias rpc]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc]
Expand Down Expand Up @@ -172,6 +173,12 @@
(map decode-row)
(map process-permissions)))

(defn- add-org-to-team
[cfg team params]
(let [params (assoc (or params {}) :team-id (:id team))
org (nitrate/call cfg :get-team-org params)]
(assoc team :organization-id (:id org) :organization-name (:name org))))

(defn get-teams
[conn profile-id]
(let [profile (profile/get-profile conn profile-id)
Expand All @@ -190,7 +197,9 @@
::sm/params schema:get-teams}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(dm/with-open [conn (db/open pool)]
(get-teams conn profile-id)))
(cond->> (get-teams conn profile-id)
(contains? cf/flags :nitrate)
(map #(add-org-to-team cfg % params)))))

(def ^:private sql:get-owned-teams
"SELECT t.id, t.name,
Expand Down
114 changes: 114 additions & 0 deletions backend/src/app/rpc/management/nitrate.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC

(ns app.rpc.management.nitrate
"Internal Nitrate HTTP API.
Provides authenticated access to organization management and token validation endpoints.
All requests must include a valid shared key token in the `x-shared-key` header, and
a cookie `auth-token` with the user token.
They will return `401 Unauthorized` if the shared key or user token are invalid."
(:require
[app.common.schema :as sm]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.msgbus :as mbus]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as doc]
[app.tokens :as tokens]
[app.util.services :as sv]
[cuerdas.core :as str]))

;; ---- API: authenticate
(def ^:private schema:profile
[:map
[:id ::sm/uuid]
[:name :string]
[:email :string]
[:photo-url :string]])

(sv/defmethod ::authenticate
"Authenticate an user
@api GET /authenticate
@returns
200 OK: Returns the authenticated user."
{::doc/added "2.12"
::sm/result schema:profile}
[cfg {:keys [::rpc/profile-id] :as params}]
(let [profile (profile/get-profile cfg profile-id)]
{:id (get profile :id)
:name (get profile :fullname)
:email (get profile :email)
:photo-url (files/resolve-public-uri (get profile :photo-id))}))

;; ---- API: get-teams

(def ^:private sql:get-teams
"SELECT t.*
FROM team AS t
JOIN team_profile_rel AS tpr ON t.id = tpr.team_id
WHERE tpr.profile_id = ?
AND tpr.is_owner = 't'
AND t.is_default = 'f'
AND t.deleted_at is null;")

(def ^:private schema:team
[:map
[:id ::sm/uuid]
[:name :string]])

(def ^:private schema:get-teams-result
[:vector schema:team])

(sv/defmethod ::get-teams
"List teams for which current user is owner.
@api GET /get-teams
@returns
200 OK: Returns the list of teams for the user."
{::doc/added "2.12"
::sm/result schema:get-teams-result}
[cfg {:keys [::rpc/profile-id]}]
(when (contains? cf/flags :nitrate)
(let [current-user-id (-> (profile/get-profile cfg profile-id) :id)]
(->> (db/exec! cfg [sql:get-teams current-user-id])
(map #(select-keys % [:id :name]))))))

;; ---- API: notify-team-change

(def ^:private schema:notify-team-change
[:map
[:id ::sm/uuid]
[:organization-id ::sm/int]])


(sv/defmethod ::notify-team-change
"Notify to Penpot a team change from nitrate
@api POST /notify-team-change
@returns
200 OK"
{::doc/added "2.12"
::sm/params schema:notify-team-change
::rpc/auth false}
[cfg {:keys [id organization-id organization-name]}]
(when (contains? cf/flags :nitrate)
(let [msgbus (::mbus/msgbus cfg)]
(mbus/pub! msgbus
;;TODO There is a bug on dashboard with teams notifications.
;;For now we send it to uuid/zero instead of team-id
:topic uuid/zero
:message {:type :team-org-change
:team-id id
:organization-id organization-id
:organization-name organization-name}))))







5 changes: 4 additions & 1 deletion common/src/app/common/flags.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,10 @@

;; A temporal flag, enables backend code use more extensivelly
;; redis for caching data
:redis-cache})
:redis-cache

;; Activates the nitrate module
:nitrate})

(def all-flags
(set/union email login varia))
Expand Down
5 changes: 5 additions & 0 deletions docker/devenv/files/start-tmux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,9 @@ tmux select-window -t penpot:4
tmux send-keys -t penpot 'cd penpot/backend' enter C-l
tmux send-keys -t penpot './scripts/start-dev' enter

tmux new-window -t penpot:5 -n 'nitrate'
tmux select-window -t penpot:5
tmux send-keys -t penpot 'cd penpot/penpot-nitrate' enter C-l
tmux send-keys -t penpot 'pnpm dev --host' enter

tmux -2 attach-session -t penpot
12 changes: 12 additions & 0 deletions frontend/src/app/main/data/dashboard.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -649,10 +649,22 @@
(rx/of (dcm/change-team-role params)
(modal/hide)))))

(defn handle-change-team-org
[{:keys [team-id organization-id organization-name] :as message}]
(ptk/reify ::handle-change-team-org
ptk/UpdateEvent
(update [_ state]
(if (contains? (:teams state) team-id)
(-> state
(assoc-in [:teams team-id :organization-id] organization-id)
(assoc-in [:teams team-id :organization-name] organization-name))
state))))

(defn- process-message
[{:keys [type] :as msg}]
(case type
:notification (dcm/handle-notification msg)
:team-role-change (handle-change-team-role msg)
:team-membership-change (dcm/team-membership-change msg)
:team-org-change (handle-change-team-org msg)
nil))
Loading
Loading