From ff9506c551475e03be1ad9ab96f718c006a3a53b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Valderrama?= Date: Wed, 28 Oct 2026 11:02:42 +0100 Subject: [PATCH] :sparkles: Add member profile support for organization attributes (#12652) --- .../src/app/loggers/audit/consumer_task.clj | 2 + backend/src/app/rpc/management/nitrate.clj | 51 ++++++++++++++++++- .../rpc_management_nitrate_test.clj | 47 +++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/backend/src/app/loggers/audit/consumer_task.clj b/backend/src/app/loggers/audit/consumer_task.clj index 7270cfae94..3180f936f0 100644 --- a/backend/src/app/loggers/audit/consumer_task.clj +++ b/backend/src/app/loggers/audit/consumer_task.clj @@ -213,6 +213,7 @@ "accept-team-invitation-from" "add-member-to-organization" "add-team-to-organization" + "assign-organization-attribute" "auditlog-exported" "auditlog-guide-downloaded" "cancel-organization-invitation" @@ -253,6 +254,7 @@ "sso-config-tested" "success-activate-organization-sso" "success-apply-organization-sso-changes" + "unassign-organization-attribute" "update-organization-invitation" "update-organization-permissions" "update-team-invitation" diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index c8c4f83e8c..20cd899ae7 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -17,7 +17,7 @@ [app.common.time :as ct] [app.common.types.organization :as cto] [app.common.types.profile :refer [schema:profile, schema:basic-profile]] - [app.common.types.team :refer [schema:team]] + [app.common.types.team :as ctt :refer [schema:team]] [app.common.uri :as u] [app.common.uuid :as uuid] [app.config :as cf] @@ -562,6 +562,55 @@ RETURNING id, deleted_at;") (db/exec! conn [sql:get-organization-member-team-counts ids-array]))))))) +;; ---- API: get-organization-member-teams + +(def ^:private sql:get-organization-member-teams + "SELECT t.id, t.name, t.photo_id, + CASE WHEN tpr.is_owner THEN 'owner' + WHEN tpr.is_admin THEN 'admin' + WHEN tpr.can_edit THEN 'editor' + ELSE 'viewer' + END AS role + FROM team_profile_rel AS tpr + JOIN team AS t ON t.id = tpr.team_id + WHERE tpr.profile_id = ? + AND t.id = ANY(?) + AND t.deleted_at IS NULL + AND t.is_default IS FALSE;") + +(def ^:private schema:get-organization-member-teams-params + [:map + [:profile-id ::sm/uuid] + [:team-ids [:or ::sm/uuid [:vector ::sm/uuid]]]]) + +(def ^:private schema:get-organization-member-teams-result + [:vector [:map + [:id ::sm/uuid] + [:name ::sm/text] + [:photo-url {:optional true} ::sm/uri] + [:role ctt/schema:role]]]) + +(sv/defmethod ::get-organization-member-teams + "Get the non-default teams a profile belongs to within a set of teams, + with the profile's role in each one." + {::doc/added "2.18" + ::sm/params schema:get-organization-member-teams-params + ::sm/result schema:get-organization-member-teams-result + ::nitrate/sso false} + [cfg {:keys [profile-id team-ids]}] + (let [team-ids (if (uuid? team-ids) [team-ids] team-ids)] + (if (empty? team-ids) + [] + (db/run! cfg (fn [{:keys [::db/conn]}] + (let [ids-array (db/create-array conn "uuid" team-ids)] + (->> (db/exec! conn [sql:get-organization-member-teams profile-id ids-array]) + (mapv (fn [{:keys [photo-id role] :as row}] + (cond-> (-> row + (dissoc :photo-id) + (assoc :role (keyword role))) + photo-id (assoc :photo-url (files/resolve-public-uri photo-id)))))))))))) + + ;; API: invite-to-organization (defn- get-invitation-organization diff --git a/backend/test/backend_tests/rpc_management_nitrate_test.clj b/backend/test/backend_tests/rpc_management_nitrate_test.clj index 500b241279..2d411bf816 100644 --- a/backend/test/backend_tests/rpc_management_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_management_nitrate_test.clj @@ -251,6 +251,53 @@ (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 get-organization-member-teams-returns-roles-within-given-teams + (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}) + owned-team (th/create-team* 1 {:profile-id (:id profile)}) + admin-team (th/create-team* 2 {:profile-id (:id other)}) + editor-team (th/create-team* 3 {:profile-id (:id other)}) + viewer-team (th/create-team* 4 {:profile-id (:id other)}) + foreign-team (th/create-team* 5 {:profile-id (:id other)}) + not-member (th/create-team* 6 {:profile-id (:id other)}) + deleted-team (th/create-team* 7 {:profile-id (:id profile)}) + _ (th/db-update! :team + {:deleted-at (ct/now)} + {:id (:id deleted-team)}) + _ (doseq [[team role] [[admin-team :admin] + [editor-team :editor] + [viewer-team :viewer] + [foreign-team :editor]]] + (th/create-team-role* {:team-id (:id team) + :profile-id (:id profile) + :role role})) + team-ids (mapv :id [owned-team admin-team editor-team viewer-team + not-member deleted-team + {:id (:default-team-id profile)}]) + out (th/management-command! {::th/type :get-organization-member-teams + ::rpc/profile-id (:id other) + :profile-id (:id profile) + :team-ids team-ids})] + (t/is (th/success? out)) + (t/is (= {(:id owned-team) :owner + (:id admin-team) :admin + (:id editor-team) :editor + (:id viewer-team) :viewer} + (->> out :result (map (juxt :id :role)) (into {})))) + (t/is (= (:name owned-team) + (->> out :result (d/seek #(= (:id owned-team) (:id %))) :name)))))) + +(t/deftest get-organization-member-teams-empty-team-ids + (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 :get-organization-member-teams + ::rpc/profile-id (:id profile) + :profile-id (:id profile) + :team-ids []})] + (t/is (th/success? out)) + (t/is (= [] (:result out)))))) + (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