From 90ab142f003499afe383ebdd9e50ede6ac31843a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Tue, 29 Sep 2026 16:05:27 +0200 Subject: [PATCH] :bug: Add nitrate management methods (#11962) --- backend/src/app/rpc/management/nitrate.clj | 59 +++++++++++++++ .../rpc_management_nitrate_test.clj | 73 +++++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 36e37aca9d..23a11f8151 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -78,6 +78,13 @@ AND t.is_default IS FALSE AND t.deleted_at IS NULL;") +(def ^:private sql:get-member-teams + "SELECT t.id, t.name, t.is_default + FROM team AS t + JOIN team_profile_rel AS tpr ON t.id = tpr.team_id + WHERE tpr.profile_id = ? + AND t.deleted_at IS NULL;") + ;; ---- API: get-penpot-version (def ^:private schema:get-penpot-version-result @@ -107,6 +114,13 @@ (def ^:private schema:get-teams-result [:vector schema:team]) +(def ^:private schema:get-member-teams-result + [:vector + [:map + [:id ::sm/uuid] + [:name ::sm/text] + [:is-default ::sm/boolean]]]) + (sv/defmethod ::get-teams "List teams for which current user is owner" {::doc/added "2.18" @@ -118,6 +132,51 @@ (->> (db/exec! cfg [sql:get-teams current-user-id]) (map #(select-keys % [:id :name]))))) +(sv/defmethod ::get-member-teams + "List all active teams where the current user is a member" + {::doc/added "2.18" + ::sm/params [:map] + ::sm/result schema:get-member-teams-result + ::nitrate/sso false} + [cfg {:keys [::rpc/profile-id]}] + (let [current-user-id (-> (profile/get-profile cfg profile-id) :id)] + (db/exec! cfg [sql:get-member-teams current-user-id]))) + +;; ---- API: update-profile-theme + +(def ^:private schema:update-profile-theme + [:map {:title "update-profile-theme"} + [:theme [:enum "light" "dark" "system"]]]) + +(sv/defmethod ::update-profile-theme + "Update the current user's theme" + {::doc/added "2.18" + ::sm/params schema:update-profile-theme + ::db/transaction true + ::nitrate/sso false} + [{:keys [::db/conn]} {:keys [::rpc/profile-id theme]}] + (profile/get-profile conn profile-id ::db/for-update true) + (db/update! conn :profile + {:theme theme} + {:id profile-id} + {::db/return-keys false}) + nil) + +;; ---- API: update-profile-props + +(def ^:private schema:update-profile-props + [:map {:title "update-profile-props"} + [:props profile/schema:props-writeable]]) + +(sv/defmethod ::update-profile-props + "Merge writable properties into the current user's profile" + {::doc/added "2.18" + ::sm/params schema:update-profile-props + ::db/transaction true + ::nitrate/sso false} + [cfg {:keys [::rpc/profile-id props]}] + (profile/update-profile-props cfg profile-id props)) + ;; ---- API: upload-organization-logo (def ^:private schema:upload-organization-logo diff --git a/backend/test/backend_tests/rpc_management_nitrate_test.clj b/backend/test/backend_tests/rpc_management_nitrate_test.clj index 3aa81b7a2b..68fa47bbdd 100644 --- a/backend/test/backend_tests/rpc_management_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_management_nitrate_test.clj @@ -17,6 +17,7 @@ [app.msgbus :as mbus] [app.nitrate :as nitrate] [app.rpc :as-alias rpc] + [app.rpc.commands.profile :as profile] [app.util.ssrf :as ssrf] [app.worker :as wrk] [backend-tests.helpers :as th] @@ -225,6 +226,78 @@ (t/is (= #{(:name owned-team)} (->> out :result (map :name) set)))))) +(t/deftest get-member-teams-returns-all-active-memberships + (with-mocks [nitrate-mock {:target 'app.nitrate/call :return nil}] + (let [profile (th/create-profile* 1 {:is-active true}) + other (th/create-profile* 2 {:is-active true}) + default-team (th/db-get :team {:id (:default-team-id profile)}) + owned-team (th/create-team* 1 {:profile-id (:id profile)}) + member-team (th/create-team* 2 {:profile-id (:id other)}) + _ (th/create-team-role* {:team-id (:id member-team) + :profile-id (:id profile) + :role :editor}) + deleted-team (th/create-team* 3 {:profile-id (:id profile)}) + _ (th/db-update! :team + {:deleted-at (ct/now)} + {:id (:id deleted-team)}) + out (th/management-command! {::th/type :get-member-teams + ::rpc/profile-id (:id profile)})] + (t/is (th/success? out)) + (let [teams-by-id (->> out :result (d/index-by :id))] + (t/is (= #{(:id default-team) (:id owned-team) (:id member-team)} + (set (keys teams-by-id)))) + (t/is (true? (get-in teams-by-id [(:id default-team) :is-default]))) + (t/is (false? (get-in teams-by-id [(:id owned-team) :is-default]))) + (t/is (false? (get-in teams-by-id [(:id member-team) :is-default]))))))) + +(t/deftest update-profile-theme-updates-only-theme + (with-mocks [nitrate-mock {:target 'app.nitrate/call :return nil}] + (let [profile (th/create-profile* 1 {:is-active true + :fullname "Nitrate User" + :lang "es" + :theme "light"}) + _ (th/db-update! :profile + {:lang "es"} + {:id (:id profile)}) + out (th/management-command! {::th/type :update-profile-theme + ::rpc/profile-id (:id profile) + :theme "dark"}) + saved (-> (th/db-get :profile {:id (:id profile)}) + (profile/decode-row))] + (t/is (th/success? out)) + (t/is (= "dark" (:theme saved))) + (t/is (= "Nitrate User" (:fullname saved))) + (t/is (= "es" (:lang saved)))))) + +(t/deftest update-profile-theme-rejects-invalid-theme + (with-mocks [nitrate-mock {:target 'app.nitrate/call :return nil}] + (let [profile (th/create-profile* 1 {:is-active true}) + out (th/management-command! {::th/type :update-profile-theme + ::rpc/profile-id (:id profile) + :theme "invalid"}) + error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :validation)) + (t/is (th/ex-of-code? error :params-validation))))) + +(t/deftest update-profile-props-merges-onboarding-props + (with-mocks [nitrate-mock {:target 'app.nitrate/call :return nil}] + (let [profile (th/create-profile* 1 {:is-active true}) + out (th/management-command! {::th/type :update-profile-props + ::rpc/profile-id (:id profile) + :props {:nitrate-onboarding-viewed true + :onboarding-questions-answered true + :onboarding-questions + {:role "developer" + :company-size "2-100"}}}) + saved (-> (th/db-get :profile {:id (:id profile)}) + (profile/decode-row))] + (t/is (th/success? out)) + (t/is (true? (get-in saved [:props :nitrate-onboarding-viewed]))) + (t/is (true? (get-in saved [:props :onboarding-questions-answered]))) + (t/is (= {:role "developer" :company-size "2-100"} + (get-in saved [:props :onboarding-questions])))))) + (t/deftest notify-team-change-publishes-event (let [team-id (uuid/random) organization-id (uuid/random)