mirror of
https://github.com/penpot/penpot.git
synced 2026-01-27 07:42:03 -05:00
Compare commits
6 Commits
xaviju-132
...
niwinz-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f513c1d10 | ||
|
|
cafc4567b3 | ||
|
|
9ea7d0e243 | ||
|
|
da029de7e4 | ||
|
|
10c08c88c7 | ||
|
|
877b6630f3 |
@@ -1,7 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
|
||||
export PENPOT_NITRATE_SHARED_KEY=super-secret-nitrate-api-key
|
||||
export PENPOT_EXPORTER_SHARED_KEY=super-secret-exporter-api-key
|
||||
export PENPOT_SECRET_KEY=super-secret-devenv-key
|
||||
|
||||
# DEPRECATED: only used for subscriptions
|
||||
export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
|
||||
|
||||
export PENPOT_HOST=devenv
|
||||
export PENPOT_PUBLIC_URI=https://localhost:3449
|
||||
|
||||
|
||||
@@ -102,6 +102,8 @@
|
||||
[:http-server-io-threads {:optional true} ::sm/int]
|
||||
[:http-server-max-worker-threads {:optional true} ::sm/int]
|
||||
|
||||
[:exporter-shared-key {:optional true} :string]
|
||||
[:nitrate-shared-key {:optional true} :string]
|
||||
[:management-api-key {:optional true} :string]
|
||||
|
||||
[:telemetry-uri {:optional true} :string]
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.middleware :as mw]
|
||||
[app.main :as-alias main]
|
||||
[app.rpc.commands.profile :as cmd.profile]
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[app.worker :as-alias wrk]
|
||||
[integrant.core :as ig]
|
||||
[yetti.request :as yreq]
|
||||
[yetti.response :as-alias yres]))
|
||||
|
||||
;; ---- ROUTES
|
||||
@@ -49,28 +49,40 @@
|
||||
(fn [cfg request]
|
||||
(db/tx-run! cfg handler request)))))})
|
||||
|
||||
(def ^:private shared-key-auth
|
||||
{:name ::shared-key-auth
|
||||
:compile
|
||||
(fn [_ _]
|
||||
(fn [handler key]
|
||||
(if key
|
||||
(fn [request]
|
||||
(if-let [key' (yreq/get-header request "x-shared-key")]
|
||||
(if (= key key')
|
||||
(handler request)
|
||||
{::yres/status 403})
|
||||
{::yres/status 403}))
|
||||
(fn [_ _]
|
||||
{::yres/status 403}))))})
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ {:keys [::setup/props] :as cfg}]
|
||||
[_ cfg]
|
||||
|
||||
(let [management-key (or (cf/get :management-api-key)
|
||||
(get props :management-key))]
|
||||
["" {:middleware [[shared-key-auth (cf/get :management-api-key)]
|
||||
[default-system cfg]
|
||||
[transaction]]}
|
||||
["/authenticate"
|
||||
{:handler authenticate
|
||||
:allowed-methods #{:post}}]
|
||||
|
||||
["" {:middleware [[mw/shared-key-auth management-key]
|
||||
[default-system cfg]
|
||||
[transaction]]}
|
||||
["/authenticate"
|
||||
{:handler authenticate
|
||||
:allowed-methods #{:post}}]
|
||||
["/get-customer"
|
||||
{:handler get-customer
|
||||
:transaction true
|
||||
:allowed-methods #{:post}}]
|
||||
|
||||
["/get-customer"
|
||||
{:handler get-customer
|
||||
:transaction true
|
||||
:allowed-methods #{:post}}]
|
||||
|
||||
["/update-customer"
|
||||
{:handler update-customer
|
||||
:allowed-methods #{:post}
|
||||
:transaction true}]]))
|
||||
["/update-customer"
|
||||
{:handler update-customer
|
||||
:allowed-methods #{:post}
|
||||
:transaction true}]])
|
||||
|
||||
;; ---- HELPERS
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
[app.http.errors :as errors]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[buddy.core.codecs :as bc]
|
||||
[cuerdas.core :as str]
|
||||
[yetti.adapter :as yt]
|
||||
[yetti.middleware :as ymw]
|
||||
@@ -301,16 +300,20 @@
|
||||
:compile (constantly wrap-auth)})
|
||||
|
||||
(defn- wrap-shared-key-auth
|
||||
[handler shared-key]
|
||||
(if shared-key
|
||||
(let [shared-key (if (string? shared-key)
|
||||
shared-key
|
||||
(bc/bytes->b64-str shared-key true))]
|
||||
(fn [request]
|
||||
(let [key (yreq/get-header request "x-shared-key")]
|
||||
(if (= key shared-key)
|
||||
(handler (assoc request ::http/auth-with-shared-key true))
|
||||
{::yres/status 403}))))
|
||||
[handler keys]
|
||||
(if (seq keys)
|
||||
(fn [request]
|
||||
(if-let [[key-id key] (some-> (yreq/get-header request "x-shared-key")
|
||||
(str/split #"\s+" 2))]
|
||||
(let [key-id (str/lower key-id)]
|
||||
(if (and (string? key)
|
||||
(contains? keys key-id)
|
||||
(= key (get keys key-id)))
|
||||
(-> request
|
||||
(assoc ::http/auth-key-id key-id)
|
||||
(handler))
|
||||
{::yres/status 403}))
|
||||
{::yres/status 403}))
|
||||
(fn [_ _]
|
||||
{::yres/status 403})))
|
||||
|
||||
|
||||
@@ -140,10 +140,14 @@
|
||||
client-version (get-client-version request)
|
||||
client-user-agent (get-client-user-agent request)
|
||||
session-id (get-external-session-id request)
|
||||
token-id (::actoken/id request)]
|
||||
key-id (::http/auth-key-id request)
|
||||
token-id (::actoken/id request)
|
||||
token-type (::actoken/type request)]
|
||||
(d/without-nils
|
||||
{:external-session-id session-id
|
||||
:initiator (or key-id "app")
|
||||
:access-token-id (some-> token-id str)
|
||||
:access-token-type (some-> token-type str)
|
||||
:client-event-origin client-event-origin
|
||||
:client-user-agent client-user-agent
|
||||
:client-version client-version
|
||||
|
||||
@@ -275,8 +275,7 @@
|
||||
::email/whitelist (ig/ref ::email/whitelist)}
|
||||
|
||||
::mgmt/routes
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::setup/props (ig/ref ::setup/props)}
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
|
||||
:app.http/router
|
||||
{::session/manager (ig/ref ::session/manager)
|
||||
@@ -341,7 +340,8 @@
|
||||
::email/whitelist (ig/ref ::email/whitelist)}
|
||||
|
||||
:app.nitrate/client
|
||||
{::http.client/client (ig/ref ::http.client/client)}
|
||||
{::http.client/client (ig/ref ::http.client/client)
|
||||
::setup/shared-keys (ig/ref ::setup/shared-keys)}
|
||||
|
||||
:app.rpc/management-methods
|
||||
{::http.client/client (ig/ref ::http.client/client)
|
||||
@@ -357,13 +357,13 @@
|
||||
::setup/props (ig/ref ::setup/props)}
|
||||
|
||||
::rpc/routes
|
||||
{::rpc/methods (ig/ref :app.rpc/methods)
|
||||
{::rpc/methods (ig/ref :app.rpc/methods)
|
||||
::rpc/management-methods (ig/ref :app.rpc/management-methods)
|
||||
|
||||
;; FIXME: revisit if db/pool is necessary here
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::session/manager (ig/ref ::session/manager)
|
||||
::setup/props (ig/ref ::setup/props)}
|
||||
::setup/shared-keys (ig/ref ::setup/shared-keys)}
|
||||
|
||||
::wrk/registry
|
||||
{::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
@@ -451,6 +451,11 @@
|
||||
;; module requires the migrations to run before initialize.
|
||||
::migrations (ig/ref :app.migrations/migrations)}
|
||||
|
||||
::setup/shared-keys
|
||||
{::setup/props (ig/ref ::setup/props)
|
||||
:nitrate (cf/get :nitrate-shared-key)
|
||||
:exporter (cf/get :exporter-shared-key)}
|
||||
|
||||
::setup/clock
|
||||
{}
|
||||
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
;; 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
|
||||
@@ -17,18 +11,17 @@
|
||||
[clojure.core :as c]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- request-builder
|
||||
[cfg method uri management-key profile-id]
|
||||
[cfg method uri shared-key profile-id]
|
||||
(fn []
|
||||
(http/req! cfg {:method method
|
||||
:headers {"content-type" "application/json"
|
||||
"accept" "application/json"
|
||||
"x-shared-key" management-key
|
||||
"x-shared-key" shared-key
|
||||
"x-profile-id" (str profile-id)}
|
||||
:uri uri
|
||||
:version :http1.1})))
|
||||
@@ -54,9 +47,9 @@
|
||||
|
||||
(defn- with-validate [handler uri schema]
|
||||
(fn []
|
||||
(let [coercer-http (sm/coercer schema
|
||||
:type :validation
|
||||
:hint (str "invalid data received calling " uri))]
|
||||
(let [coercer-http (sm/coercer schema
|
||||
:type :validation
|
||||
:hint (str "invalid data received calling " uri))]
|
||||
(try
|
||||
(coercer-http (-> (handler) :body json/decode))
|
||||
(catch Exception e
|
||||
@@ -65,8 +58,9 @@
|
||||
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)
|
||||
[cfg method uri schema {:keys [::rpc/profile-id] :as params}]
|
||||
(let [shared-key (-> cfg ::setup/shared-keys :nitrate)
|
||||
full-http-call (-> (request-builder cfg method uri shared-key profile-id)
|
||||
(with-retries 3)
|
||||
(with-validate uri schema))]
|
||||
(full-http-call)))
|
||||
@@ -103,26 +97,15 @@
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :get (str baseuri "/api/users/" (str profile-id)) schema:user 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)
|
||||
:is-valid-user (partial is-valid-user cfg)})
|
||||
{}))
|
||||
|
||||
(defmethod ig/halt-key! ::client
|
||||
[_ {:keys []}]
|
||||
(do :stuff))
|
||||
|
||||
[_ cfg]
|
||||
(when (contains? cf/flags :nitrate)
|
||||
{:get-team-org (partial get-team-org cfg)
|
||||
:is-valid-user (partial is-valid-user cfg)}))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; UTILS
|
||||
@@ -144,4 +127,4 @@
|
||||
[cfg team params]
|
||||
(let [params (assoc (or params {}) :team-id (:id team))
|
||||
org (call cfg :get-team-org params)]
|
||||
(assoc team :organization-id (:id org) :organization-name (:name org))))
|
||||
(assoc team :organization-id (:id org) :organization-name (:name org))))
|
||||
|
||||
@@ -92,11 +92,11 @@
|
||||
(fn [{:keys [params path-params method] :as request}]
|
||||
(let [handler-name (:type path-params)
|
||||
etag (yreq/get-header request "if-none-match")
|
||||
|
||||
key-id (get request ::http/auth-key-id)
|
||||
profile-id (or (::session/profile-id request)
|
||||
(::actoken/profile-id request)
|
||||
(if (::http/auth-with-shared-key request)
|
||||
uuid/zero
|
||||
nil))
|
||||
(if key-id uuid/zero nil))
|
||||
|
||||
ip-addr (inet/parse-request request)
|
||||
|
||||
@@ -298,11 +298,12 @@
|
||||
|
||||
(defn- resolve-management-methods
|
||||
[cfg]
|
||||
(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)
|
||||
(let [cfg (assoc cfg ::type "management" ::metrics-id :rpc-management-timing)
|
||||
mods (cond->> (list 'app.rpc.management.exporter)
|
||||
(contains? cf/flags :nitrate)
|
||||
(cons 'app.rpc.management.nitrate))]
|
||||
|
||||
(->> (apply sv/scan-ns mods)
|
||||
(map (partial process-method cfg "management" wrap-management))
|
||||
(into {}))))
|
||||
|
||||
@@ -346,23 +347,20 @@
|
||||
|
||||
(defmethod ig/assert-key ::routes
|
||||
[_ params]
|
||||
(assert (map? (::setup/shared-keys params)))
|
||||
(assert (db/pool? (::db/pool params)) "expect valid database pool")
|
||||
(assert (some? (::setup/props params)))
|
||||
(assert (session/manager? (::session/manager params)) "expect valid session manager")
|
||||
(assert (valid-methods? (::methods params)) "expect valid methods map")
|
||||
(assert (valid-methods? (::management-methods params)) "expect valid methods map"))
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ {:keys [::methods ::management-methods ::setup/props] :as cfg}]
|
||||
|
||||
(let [public-uri (cf/get :public-uri)
|
||||
management-key (or (cf/get :management-api-key)
|
||||
(get props :management-key))]
|
||||
[_ {:keys [::methods ::management-methods ::setup/shared-keys] :as cfg}]
|
||||
|
||||
(let [public-uri (cf/get :public-uri)]
|
||||
["/api"
|
||||
["/management"
|
||||
["/methods/:type"
|
||||
{:middleware [[mw/shared-key-auth management-key]
|
||||
{:middleware [[mw/shared-key-auth shared-keys]
|
||||
[session/authz cfg]]
|
||||
:handler (make-rpc-handler management-methods)}]
|
||||
|
||||
|
||||
@@ -5,13 +5,12 @@
|
||||
;; 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."
|
||||
"Internal Nitrate HTTP RPC API. Provides authenticated access to
|
||||
organization management and token validation endpoints."
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.profile :refer [schema:profile]]
|
||||
[app.common.types.team :refer [schema:team]]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -23,22 +22,14 @@
|
||||
[app.util.services :as sv]))
|
||||
|
||||
;; ---- 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"
|
||||
"Authenticate the current user"
|
||||
{::doc/added "2.14"
|
||||
::sm/params [:map]
|
||||
::sm/result schema:profile}
|
||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||
(let [profile (profile/get-profile cfg profile-id)]
|
||||
(let [profile (profile/get-profile cfg profile-id)]
|
||||
{:id (get profile :id)
|
||||
:name (get profile :fullname)
|
||||
:email (get profile :email)
|
||||
@@ -51,30 +42,22 @@
|
||||
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]])
|
||||
AND tpr.is_owner IS TRUE
|
||||
AND t.is_default IS FALSE
|
||||
AND t.deleted_at IS NULL;")
|
||||
|
||||
(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"
|
||||
"List teams for which current user is owner"
|
||||
{::doc/added "2.14"
|
||||
::sm/params [:map]
|
||||
::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]))))))
|
||||
(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
|
||||
|
||||
@@ -83,30 +66,18 @@
|
||||
[:id ::sm/uuid]
|
||||
[:organization-id ::sm/text]])
|
||||
|
||||
|
||||
(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"
|
||||
"Notify to Penpot a team change from nitrate"
|
||||
{::doc/added "2.14"
|
||||
::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}))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(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})))
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.rpc.management.subscription
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.time :as ct]
|
||||
[app.db :as db]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.doc :as doc]
|
||||
[app.util.services :as sv]))
|
||||
|
||||
;; ---- RPC METHOD: AUTHENTICATE
|
||||
|
||||
(def ^:private
|
||||
schema:authenticate-params
|
||||
[:map {:title "authenticate-params"}])
|
||||
|
||||
(def ^:private
|
||||
schema:authenticate-result
|
||||
[:map {:title "authenticate-result"}
|
||||
[:profile-id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::auth
|
||||
{::doc/added "2.12"
|
||||
::sm/params schema:authenticate-params
|
||||
::sm/result schema:authenticate-result}
|
||||
[_ {:keys [::rpc/profile-id]}]
|
||||
{:profile-id profile-id})
|
||||
|
||||
;; ---- RPC METHOD: GET-CUSTOMER
|
||||
|
||||
;; FIXME: move to app.common.time
|
||||
(def ^:private schema:timestamp
|
||||
(sm/type-schema
|
||||
{:type ::timestamp
|
||||
:pred ct/inst?
|
||||
:type-properties
|
||||
{:title "inst"
|
||||
:description "The same as :app.common.time/inst but encodes to epoch"
|
||||
:error/message "should be an instant"
|
||||
:gen/gen (->> (sg/small-int)
|
||||
(sg/fmap (fn [v] (ct/inst v))))
|
||||
:decode/string #(some-> % ct/inst)
|
||||
:encode/string #(some-> % inst-ms)
|
||||
:decode/json #(some-> % ct/inst)
|
||||
:encode/json #(some-> % inst-ms)}}))
|
||||
|
||||
(def ^:private schema:subscription
|
||||
[:map {:title "Subscription"}
|
||||
[:id ::sm/text]
|
||||
[:customer-id ::sm/text]
|
||||
[:type [:enum
|
||||
"unlimited"
|
||||
"professional"
|
||||
"enterprise"]]
|
||||
[:status [:enum
|
||||
"active"
|
||||
"canceled"
|
||||
"incomplete"
|
||||
"incomplete_expired"
|
||||
"past_due"
|
||||
"paused"
|
||||
"trialing"
|
||||
"unpaid"]]
|
||||
|
||||
[:billing-period [:enum
|
||||
"month"
|
||||
"day"
|
||||
"week"
|
||||
"year"]]
|
||||
[:quantity :int]
|
||||
[:description [:maybe ::sm/text]]
|
||||
[:created-at schema:timestamp]
|
||||
[:start-date [:maybe schema:timestamp]]
|
||||
[:ended-at [:maybe schema:timestamp]]
|
||||
[:trial-end [:maybe schema:timestamp]]
|
||||
[:trial-start [:maybe schema:timestamp]]
|
||||
[:cancel-at [:maybe schema:timestamp]]
|
||||
[:canceled-at [:maybe schema:timestamp]]
|
||||
[:current-period-end [:maybe schema:timestamp]]
|
||||
[:current-period-start [:maybe schema:timestamp]]
|
||||
[:cancel-at-period-end :boolean]
|
||||
|
||||
[:cancellation-details
|
||||
[:map {:title "CancellationDetails"}
|
||||
[:comment [:maybe ::sm/text]]
|
||||
[:reason [:maybe ::sm/text]]
|
||||
[:feedback [:maybe
|
||||
[:enum
|
||||
"customer_service"
|
||||
"low_quality"
|
||||
"missing_feature"
|
||||
"other"
|
||||
"switched_service"
|
||||
"too_complex"
|
||||
"too_expensive"
|
||||
"unused"]]]]]])
|
||||
|
||||
(def ^:private sql:get-customer-slots
|
||||
"WITH teams AS (
|
||||
SELECT tpr.team_id AS id,
|
||||
tpr.profile_id AS profile_id
|
||||
FROM team_profile_rel AS tpr
|
||||
WHERE tpr.is_owner IS true
|
||||
AND tpr.profile_id = ?
|
||||
), teams_with_slots AS (
|
||||
SELECT tpr.team_id AS id,
|
||||
count(*) AS total
|
||||
FROM team_profile_rel AS tpr
|
||||
WHERE tpr.team_id IN (SELECT id FROM teams)
|
||||
AND tpr.can_edit IS true
|
||||
GROUP BY 1
|
||||
ORDER BY 2
|
||||
)
|
||||
SELECT max(total) AS total FROM teams_with_slots;")
|
||||
|
||||
(defn- get-customer-slots
|
||||
[cfg profile-id]
|
||||
(let [result (db/exec-one! cfg [sql:get-customer-slots profile-id])]
|
||||
(:total result)))
|
||||
|
||||
(def ^:private schema:get-customer-params
|
||||
[:map])
|
||||
|
||||
(def ^:private schema:get-customer-result
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:num-editors ::sm/int]
|
||||
[:subscription {:optional true} schema:subscription]])
|
||||
|
||||
(sv/defmethod ::get-customer
|
||||
{::doc/added "2.12"
|
||||
::sm/params schema:get-customer-params
|
||||
::sm/result schema:get-customer-result}
|
||||
[cfg {:keys [::rpc/profile-id]}]
|
||||
(let [profile (profile/get-profile cfg profile-id)]
|
||||
{:id (get profile :id)
|
||||
:name (get profile :fullname)
|
||||
:email (get profile :email)
|
||||
:num-editors (get-customer-slots cfg profile-id)
|
||||
:subscription (-> profile :props :subscription)}))
|
||||
|
||||
|
||||
;; ---- RPC METHOD: GET-CUSTOMER
|
||||
|
||||
(def ^:private schema:update-customer-params
|
||||
[:map
|
||||
[:subscription [:maybe schema:subscription]]])
|
||||
|
||||
(def ^:private schema:update-customer-result
|
||||
[:map])
|
||||
|
||||
(sv/defmethod ::update-customer
|
||||
{::doc/added "2.12"
|
||||
::sm/params schema:update-customer-params
|
||||
::sm/result schema:update-customer-result}
|
||||
[cfg {:keys [::rpc/profile-id subscription]}]
|
||||
(let [{:keys [props] :as profile}
|
||||
(profile/get-profile cfg profile-id ::db/for-update true)
|
||||
|
||||
props
|
||||
(assoc props :subscription subscription)]
|
||||
|
||||
(l/dbg :hint "update customer"
|
||||
:profile-id (str profile-id)
|
||||
:subscription-type (get subscription :type)
|
||||
:subscription-status (get subscription :status)
|
||||
:subscription-quantity (get subscription :quantity))
|
||||
|
||||
(db/update! cfg :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id profile-id}
|
||||
{::db/return-keys false})
|
||||
|
||||
nil))
|
||||
@@ -17,6 +17,7 @@
|
||||
[app.setup.templates]
|
||||
[buddy.core.codecs :as bc]
|
||||
[buddy.core.nonce :as bn]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(defn- generate-random-key
|
||||
@@ -88,7 +89,38 @@
|
||||
(-> (get-all-props conn)
|
||||
(assoc :secret-key secret)
|
||||
(assoc :tokens-key (keys/derive secret :salt "tokens"))
|
||||
(assoc :management-key (keys/derive secret :salt "management"))
|
||||
(update :instance-id handle-instance-id conn (db/read-only? pool)))))))
|
||||
|
||||
(sm/register! ::props [:map-of :keyword ::sm/any])
|
||||
|
||||
|
||||
(defmethod ig/init-key ::shared-keys
|
||||
[_ {:keys [::props] :as cfg}]
|
||||
(let [secret (get props :secret-key)]
|
||||
(d/without-nils
|
||||
{"exporter"
|
||||
(let [key (or (get cfg :exporter)
|
||||
(-> (keys/derive secret :salt "exporter")
|
||||
(bc/bytes->b64-str true)))]
|
||||
(if (or (str/empty? key)
|
||||
(str/blank? key))
|
||||
(do
|
||||
(l/wrn :hint "exporter key is disabled because empty string found")
|
||||
nil)
|
||||
(do
|
||||
(l/inf :hint "exporter key initialized" :key (d/obfuscate-string key))
|
||||
key)))
|
||||
|
||||
"nitrate"
|
||||
(let [key (or (get cfg :nitrate)
|
||||
(-> (keys/derive secret :salt "nitrate")
|
||||
(bc/bytes->b64-str true)))]
|
||||
(if (or (str/empty? key)
|
||||
(str/blank? key))
|
||||
(do
|
||||
(l/wrn :hint "nitrate key is disabled because empty string found")
|
||||
nil)
|
||||
(do
|
||||
(l/inf :hint "nitrate key initialized" :key (d/obfuscate-string key))
|
||||
key)))})))
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
(t/deftest shared-key-auth
|
||||
(let [handler (#'app.http.middleware/wrap-shared-key-auth
|
||||
(fn [req] {::yres/status 200})
|
||||
"secret-key")]
|
||||
{"test1" "secret-key"})]
|
||||
|
||||
(let [response (handler (->DummyRequest {} {}))]
|
||||
(t/is (= 403 (::yres/status response))))
|
||||
@@ -95,6 +95,9 @@
|
||||
(t/is (= 403 (::yres/status response))))
|
||||
|
||||
(let [response (handler (->DummyRequest {"x-shared-key" "secret-key"} {}))]
|
||||
(t/is (= 403 (::yres/status response))))
|
||||
|
||||
(let [response (handler (->DummyRequest {"x-shared-key" "test1 secret-key"} {}))]
|
||||
(t/is (= 200 (::yres/status response))))))
|
||||
|
||||
(t/deftest access-token-authz
|
||||
|
||||
@@ -19,3 +19,10 @@
|
||||
|
||||
(def schema:role
|
||||
[::sm/one-of {:title "TeamRole"} valid-roles])
|
||||
|
||||
;; FIXME: specify more fields
|
||||
(def schema:team
|
||||
[:map {:title "Team"}
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]])
|
||||
|
||||
|
||||
@@ -398,7 +398,6 @@ COPY files/Caddyfile /home/
|
||||
COPY files/selfsigned.crt /home/
|
||||
COPY files/selfsigned.key /home/
|
||||
COPY files/start-tmux.sh /home/start-tmux.sh
|
||||
COPY files/start-tmux-back.sh /home/start-tmux-back.sh
|
||||
COPY files/entrypoint.sh /home/entrypoint.sh
|
||||
COPY files/init.sh /home/init.sh
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
localhost:3449 {
|
||||
reverse_proxy localhost:4449
|
||||
tls /home/selfsigned.crt /home/selfsigned.key
|
||||
header -Strict-Transport-Security
|
||||
}
|
||||
|
||||
http://localhost:3450 {
|
||||
|
||||
@@ -12,11 +12,14 @@
|
||||
["node:process" :as process]
|
||||
[app.common.data :as d]
|
||||
[app.common.flags :as flags]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.version :as v]
|
||||
[cljs.core :as c]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(l/set-level! :info)
|
||||
|
||||
(def ^:private defaults
|
||||
{:public-uri "http://localhost:3449"
|
||||
:tenant "default"
|
||||
@@ -30,7 +33,7 @@
|
||||
[:map {:title "config"}
|
||||
[:secret-key :string]
|
||||
[:public-uri {:optional true} ::sm/uri]
|
||||
[:management-api-key {:optional true} :string]
|
||||
[:exporter-shared-key {:optional true} :string]
|
||||
[:host {:optional true} :string]
|
||||
[:tenant {:optional true} :string]
|
||||
[:flags {:optional true} [::sm/set :keyword]]
|
||||
@@ -98,8 +101,10 @@
|
||||
(c/get config key default)))
|
||||
|
||||
(def management-key
|
||||
(or (c/get config :management-api-key)
|
||||
(let [secret-key (c/get config :secret-key)
|
||||
derived-key (crypto/hkdfSync "blake2b512" secret-key, "management" "" 32)]
|
||||
(-> (.from buffer/Buffer derived-key)
|
||||
(.toString "base64url")))))
|
||||
(let [key (or (c/get config :exporter-shared-key)
|
||||
(let [secret-key (c/get config :secret-key)
|
||||
derived-key (crypto/hkdfSync "blake2b512" secret-key, "exporter" "" 32)]
|
||||
(-> (.from buffer/Buffer derived-key)
|
||||
(.toString "base64url"))))]
|
||||
(l/inf :hint "exporter key initialized" :key (d/obfuscate-string key))
|
||||
key))
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
(p/mcat (fn [blob]
|
||||
(let [fdata (new http/FormData)
|
||||
agent (new http/Agent #js {:connect #js {:rejectUnauthorized false}})
|
||||
headers #js {"X-Shared-Key" cf/management-key
|
||||
headers #js {"X-Shared-Key" (str "exporter " cf/management-key)
|
||||
"Authorization" (str "Bearer " auth-token)}
|
||||
|
||||
request #js {:headers headers
|
||||
|
||||
Reference in New Issue
Block a user