diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index 741e0f836f..50e17d8eb5 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -413,6 +413,17 @@ (generate-nitrate-uri "api/connectivity") schema:connectivity params)) +(def ^:private schema:identity + [:map + [:nitrate-id ::sm/text] + [:public-key ::sm/text]]) + +(defn- get-identity-api + [cfg params] + (request-to-nitrate cfg :get + (generate-nitrate-uri "api/identity") + schema:identity params)) + (def ^:private schema:redeem-result [:map [:cancel-at [:maybe schema:timestamp]]]) @@ -491,6 +502,7 @@ :get-subscription (partial get-subscription-api cfg) :get-subscription-warning (partial get-subscription-warning-api cfg) :connectivity (partial get-connectivity-api cfg) + :get-identity (partial get-identity-api cfg) :redeem-activation-code (partial redeem-activation-code-api cfg)})) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index 71a2e93b17..ffe0423c01 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -11,6 +11,7 @@ [app.auth.oidc :as oidc] [app.common.data :as d] [app.common.exceptions :as ex] + [app.common.json :as json] [app.common.schema :as sm] [app.common.time :as ct] [app.common.types.nitrate-permissions :as nitrate-perms] @@ -24,7 +25,8 @@ [app.rpc.nitrate.emails-helper :as neh] [app.rpc.nitrate.organization-helper :as noh] [app.rpc.notifications :as notifications] - [app.util.services :as sv])) + [app.util.services :as sv] + [buddy.core.codecs :as bc])) (defn assert-is-owner [cfg profile-id team-id] @@ -114,6 +116,35 @@ :cause cause) (throw cause))))))) +(def ^:private activation-code-request-filename + "penpot-activation-code-request.txt") + +(sv/defmethod ::get-nitrate-activation-code-request + "Returns a Base64-encoded JSON file requesting a Nitrate activation code. + Payload includes nitrateId, publicKey, email and iat." + {::rpc/auth true + ::doc/added "2.20" + ::sm/params [:map] + ::sm/result ::sm/text} + [cfg {:keys [::rpc/profile-id]}] + (let [profile (db/get cfg :profile {:id profile-id}) + nitrate-identity (nitrate/call cfg :get-identity {})] + (when-not nitrate-identity + (ex/raise :type :validation + :code :nitrate-identity-unavailable + :hint "Unable to retrieve nitrate identity")) + (-> (json/encode {:nitrate-id (:nitrate-id nitrate-identity) + :public-key (:public-key nitrate-identity) + :email (:email profile) + :iat (ct/seconds (ct/now))} + :key-fn json/write-camel-key) + (bc/str->bytes) + (bc/bytes->b64-str) + (rph/wrap) + (rph/with-header "content-type" "text/plain") + (rph/with-header "content-disposition" + (str "attachment; filename=\"" activation-code-request-filename "\""))))) + (def ^:private sql:prefix-team-name-and-unset-default "UPDATE team SET name = ? || name, diff --git a/backend/test/backend_tests/rpc_nitrate_test.clj b/backend/test/backend_tests/rpc_nitrate_test.clj index 28015bb369..f482470eb6 100644 --- a/backend/test/backend_tests/rpc_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_nitrate_test.clj @@ -6,16 +6,20 @@ (ns backend-tests.rpc-nitrate-test (:require + [app.common.json :as json] [app.common.time :as ct] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as-alias db] [app.email :as eml] + [app.http :as-alias http] [app.nitrate :as nitrate] [app.rpc :as-alias rpc] [app.rpc.commands.nitrate] [app.rpc.commands.teams :as teams] + [app.rpc.helpers :as rph] [backend-tests.helpers :as th] + [buddy.core.codecs :as bc] [clojure.test :as t] [cuerdas.core :as str])) @@ -933,3 +937,44 @@ :organization-id org-id})] (t/is (th/success? out)) (t/is (empty? @sent)))))) + +(t/deftest get-nitrate-activation-code-request + (let [profile (th/create-profile* 1 {:is-active true}) + nitrate-id "nitrate-instance-1" + public-key "-----BEGIN PUBLIC KEY-----\nMIIB\n-----END PUBLIC KEY-----" + now (ct/now)] + (with-redefs [cf/flags (conj cf/flags :nitrate) + ct/now (constantly now) + nitrate/call (fn [_cfg method _params] + (t/is (= :get-identity method)) + {:nitrate-id nitrate-id + :public-key public-key})] + (let [out (th/command! {::th/type :get-nitrate-activation-code-request + ::rpc/profile-id (:id profile)}) + body (-> (:result out) + (bc/b64->str) + (json/decode :key-fn json/read-kebab-key))] + (t/is (th/success? out)) + (t/is (= {:nitrate-id nitrate-id + :public-key public-key + :email (:email profile) + :iat (ct/seconds now)} + body)) + + (let [[_ method-fn] (get-in th/*system* [:app.rpc/methods :get-nitrate-activation-code-request]) + result (method-fn {::rpc/profile-id (:id profile) + ::rpc/request-at now}) + headers (::http/headers (meta result))] + (t/is (rph/wrapped? result)) + (t/is (= "text/plain" (get headers "content-type"))) + (t/is (= "attachment; filename=\"penpot-activation-code-request.txt\"" + (get headers "content-disposition")))))))) + +(t/deftest get-nitrate-activation-code-request-identity-unavailable + (let [profile (th/create-profile* 1 {:is-active true})] + (with-redefs [cf/flags (conj cf/flags :nitrate) + nitrate/call (fn [_cfg _method _params] nil)] + (let [out (th/command! {::th/type :get-nitrate-activation-code-request + ::rpc/profile-id (:id profile)})] + (t/is (not (th/success? out))) + (t/is (th/ex-of-code? (:error out) :nitrate-identity-unavailable)))))) diff --git a/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs b/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs index 95d2de2256..7ad802a2b8 100644 --- a/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs +++ b/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs @@ -17,6 +17,7 @@ [app.main.ui.nitrate.nitrate-activation-success-modal] [app.util.dom :as dom] [app.util.i18n :refer [tr]] + [app.util.webapi :as wapi] [beicon.v2.core :as rx] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -63,7 +64,16 @@ (mf/deps on-accept) (fn [event] (when (and (= "Enter" (.-key event)) (.-ctrlKey event)) - (on-accept event))))] + (on-accept event)))) + + on-download-request-click + (mf/use-fn + (fn [] + (->> (rp/cmd! :get-nitrate-activation-code-request {}) + (rx/subs! + (fn [body] + (->> (wapi/create-blob body "text/plain") + (dom/trigger-download "penpot-activation-code-request.txt")))))))] [:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-dialog)} @@ -101,7 +111,11 @@ :value (tr "nitrate.code-activation.submit") :on-click on-accept}]] [:div {:class (stl/css :footer-text)} - (tr "nitrate.code-activation.footer") " " + (tr "nitrate.code-activation.footer-before") + [:a {:class (stl/css :link) + :on-click on-download-request-click} + (tr "nitrate.code-activation.footer-link")] + (tr "nitrate.code-activation.footer-after") " " [:a {:class (stl/css :link) :href "mailto:sales@nitrate.com"} "sales@nitrate.com"]]]]])) diff --git a/frontend/src/app/main/ui/nitrate/nitrate_form.cljs b/frontend/src/app/main/ui/nitrate/nitrate_form.cljs index 6d54c2e4c3..a3fa606b78 100644 --- a/frontend/src/app/main/ui/nitrate/nitrate_form.cljs +++ b/frontend/src/app/main/ui/nitrate/nitrate_form.cljs @@ -84,9 +84,9 @@ (tr "nitrate.form.cancel-anytime")]]] [:p {:class (stl/css :modal-text-medium)} - (tr "nitrate.form.have-code") " " [:a {:class (stl/css :link) - :on-click on-activate-click} - (tr "nitrate.form.enter-code")]] + (tr "nitrate.form.subscribe-with-code") " " [:a {:class (stl/css :link) + :on-click on-activate-click} + (tr "nitrate.form.enter-code")]] [:p {:class (stl/css :modal-text-medium)} [:a {:class (stl/css :link) :href dnt/go-to-subscription-url} @@ -102,7 +102,7 @@ "sales@penpot.app"]] [:div {:class (stl/css :activation-code)} [:p {:class (stl/css :modal-text-large)} - (tr "nitrate.form.have-code")] + (tr "nitrate.form.subscribe-with-code")] [:p {:class (stl/css :modal-text-large)} [:a {:class (stl/css :link) :on-click on-activate-click} diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 243d3298f8..3799d37643 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -4372,8 +4372,14 @@ msgid "nitrate.modal-success.title" msgstr "Welcome to Enterprise!" #: src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs:104 -msgid "nitrate.code-activation.footer" -msgstr "Need a code? Contact us:" +msgid "nitrate.code-activation.footer-before" +msgstr "Need a code? Download your " + +msgid "nitrate.code-activation.footer-link" +msgstr "activation code request" + +msgid "nitrate.code-activation.footer-after" +msgstr " and contact us:" #: src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs:86 msgid "nitrate.code-activation.input-label" @@ -4473,8 +4479,8 @@ msgid "nitrate.form.enterprise.price" msgstr "per member per month" #: src/app/main/ui/nitrate/nitrate_form.cljs:80, src/app/main/ui/nitrate/nitrate_form.cljs:98 -msgid "nitrate.form.have-code" -msgstr "Have an activation code?" +msgid "nitrate.form.subscribe-with-code" +msgstr "Subscribe with an activation code" #: src/app/main/ui/nitrate/nitrate_form.cljs:86 msgid "nitrate.form.see-plan" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 9f80519eea..0660023ff4 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -4247,8 +4247,14 @@ msgid "nitrate.modal-success.title" msgstr "¡Bienvenido a Enterprise!" #: src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs:104 -msgid "nitrate.code-activation.footer" -msgstr "¿Necesitas un código? Contáctanos:" +msgid "nitrate.code-activation.footer-before" +msgstr "¿Necesitas un código? Descarga tu " + +msgid "nitrate.code-activation.footer-link" +msgstr "solicitud de código de activación" + +msgid "nitrate.code-activation.footer-after" +msgstr " y contáctanos:" #: src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs:86 msgid "nitrate.code-activation.input-label" @@ -4351,8 +4357,8 @@ msgid "nitrate.form.enterprise.price" msgstr "por miembro al mes" #: src/app/main/ui/nitrate/nitrate_form.cljs:80, src/app/main/ui/nitrate/nitrate_form.cljs:98 -msgid "nitrate.form.have-code" -msgstr "¿Tienes un código de activación?" +msgid "nitrate.form.subscribe-with-code" +msgstr "Suscribete con un código de activación" #: src/app/main/ui/nitrate/nitrate_form.cljs:86 msgid "nitrate.form.see-plan"