diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index ba052d4477..000a55953c 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -54,10 +54,11 @@ (if (>= status 400) ;; For error status codes (4xx, 5xx), fail immediately without validation (do - (l/error :hint "nitrate request failed with error status" - :uri uri - :status status - :body (:body response)) + (when (not= status 404) ;; Don't need to log 404 + (l/error :hint "nitrate request failed with error status" + :uri uri + :status status + :body (:body response))) nil) ;; For success status codes, validate the response (let [coercer-http (sm/coercer schema @@ -107,6 +108,11 @@ [:organization-id ::sm/uuid] [:is-your-penpot :boolean]]) +(def ^:private schema:profile-org + [:map + [:is-member :boolean] + [:organization-id ::sm/uuid]]) + ;; TODO Unify with schemas on backend/src/app/http/management.clj (def ^:private schema:timestamp (sm/type-schema @@ -179,7 +185,7 @@ [:map [:licenses ::sm/boolean]]) -(defn- get-team-org +(defn- get-team-org-api [cfg {:keys [team-id] :as params}] (let [baseuri (cf/get :nitrate-backend-uri)] (request-to-nitrate cfg :get @@ -188,7 +194,18 @@ team-id) schema:organization params))) -(defn- set-team-org +(defn- get-org-membership-by-team-api + [cfg {:keys [profile-id team-id] :as params}] + (let [baseuri (cf/get :nitrate-backend-uri)] + (request-to-nitrate cfg :get + (str baseuri + "/api/teams/" + team-id + "/users/" + profile-id) + schema:profile-org params))) + +(defn- set-team-org-api [cfg {:keys [organization-id team-id is-default] :as params}] (let [baseuri (cf/get :nitrate-backend-uri) params (assoc params :request-params {:team-id team-id @@ -200,7 +217,18 @@ "/add-team") schema:team params))) -(defn- get-subscription +(defn- add-profile-to-org-api + [cfg {:keys [profile-id org-id team-id] :as params}] + (let [baseuri (cf/get :nitrate-backend-uri) + params (assoc params :request-params {:user-id profile-id :team-id team-id})] + (request-to-nitrate cfg :post + (str baseuri + "/api/organizations/" + org-id + "/add-user") + schema:profile-org params))) + +(defn- get-subscription-api [cfg {:keys [profile-id] :as params}] (let [baseuri (cf/get :nitrate-backend-uri)] (request-to-nitrate cfg :get @@ -209,7 +237,7 @@ profile-id) schema:subscription params))) -(defn- get-connectivity +(defn- get-connectivity-api [cfg params] (let [baseuri (cf/get :nitrate-backend-uri)] (request-to-nitrate cfg :get @@ -224,10 +252,12 @@ (defmethod ig/init-key ::client [_ cfg] (when (contains? cf/flags :nitrate) - {:get-team-org (partial get-team-org cfg) - :set-team-org (partial set-team-org cfg) - :get-subscription (partial get-subscription cfg) - :connectivity (partial get-connectivity cfg)})) + {:get-team-org (partial get-team-org-api cfg) + :set-team-org (partial set-team-org-api cfg) + :get-org-membership-by-team (partial get-org-membership-by-team-api cfg) + :add-profile-to-org (partial add-profile-to-org-api cfg) + :get-subscription (partial get-subscription-api cfg) + :connectivity (partial get-connectivity-api cfg)})) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; UTILS @@ -287,6 +317,9 @@ :organization-id (:organization-id params)}))) team)) + + (defn connectivity [cfg] (call cfg :connectivity {})) + diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index fb5db45ef7..10639970ef 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -371,9 +371,11 @@ (defn create-profile-rels - [conn {:keys [id] :as profile}] + [{:keys [::db/conn] :as cfg} {:keys [id] :as profile}] + (assert (db/connection-map? cfg) + "expected cfg with valid connection") (let [features (cfeat/get-enabled-features cf/flags) - team (teams/create-team conn + team (teams/create-team cfg {:profile-id id :name "Default" :features features @@ -426,7 +428,7 @@ (assoc :is-active is-active) (update :password auth/derive-password)) profile (->> (create-profile cfg params) - (create-profile-rels conn))] + (create-profile-rels cfg))] (vary-meta profile assoc :created true)))) created? (-> profile meta :created true?) diff --git a/backend/src/app/rpc/commands/demo.clj b/backend/src/app/rpc/commands/demo.clj index d4f46e750b..f3ff979113 100644 --- a/backend/src/app/rpc/commands/demo.clj +++ b/backend/src/app/rpc/commands/demo.clj @@ -49,9 +49,9 @@ :deleted-at (ct/in-future (cf/get-deletion-delay)) :password (derive-password password) :props {}} - profile (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + profile (db/tx-run! cfg (fn [cfg] (->> (auth/create-profile cfg params) - (auth/create-profile-rels conn))))] + (auth/create-profile-rels cfg))))] (with-meta {:email email :password password} {::audit/profile-id (:id profile)}))) diff --git a/backend/src/app/rpc/commands/ldap.clj b/backend/src/app/rpc/commands/ldap.clj index 5aa2a21935..f4aea5bc10 100644 --- a/backend/src/app/rpc/commands/ldap.clj +++ b/backend/src/app/rpc/commands/ldap.clj @@ -84,5 +84,5 @@ (profile/get-profile-by-email conn)) (->> (assoc info :is-active true :is-demo false) (auth/create-profile cfg) - (auth/create-profile-rels conn) + (auth/create-profile-rels cfg) (profile/strip-private-attrs)))))) diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj index c4d580c37d..d078983a27 100644 --- a/backend/src/app/rpc/commands/management.clj +++ b/backend/src/app/rpc/commands/management.clj @@ -207,8 +207,7 @@ (update :team-id bfc/lookup-index) (assoc :created-at timestamp) (assoc :modified-at timestamp))] - (db/insert! conn :team-profile-rel params - {::db/return-keys false}))) + (teams/add-profile-to-team! cfg params {::db/return-keys false}))) ;; Duplicate team fonts (doseq [font fonts] diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 55836cb5b6..854b36bb32 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -522,20 +522,78 @@ (with-meta team {::audit/props {:id (:id team)}}))) + +(defn create-default-org-team + [cfg profile-id organization-id] + (quotes/check! cfg {::quotes/id ::quotes/teams-per-profile + ::quotes/profile-id profile-id}) + + (let [features (-> (cfeat/get-enabled-features cf/flags) + (set/difference cfeat/frontend-only-features) + (set/difference cfeat/no-team-inheritable-features)) + params {:profile-id profile-id + :name "Default" + :features features + :organization-id organization-id + :is-default true} + team (create-team cfg params)] + (select-keys team [:id]))) + +(defn- initialize-user-in-nitrate-org + "If needed, create a default team for the user on the organization, + and notify Nitrate that an user has been added to an org." + [cfg profile-id team-id] + (assert (db/connection-map? cfg) + "expected cfg with valid connection") + (let [membership (nitrate/call cfg :get-org-membership-by-team {:profile-id profile-id :team-id team-id})] + ;; Only when the team belong to an organization and the user is not a member + (when (and + (some? (:organization-id membership)) ;; the team do belong to an organization + (not (:is-member membership))) ;; the user is not a member of the org yet + + (db/tx-run! + cfg + (fn [{:keys [::db/conn] :as tx-cfg}] + (let [org-id (:organization-id membership) + default-team (create-default-org-team (assoc tx-cfg ::db/conn conn) profile-id org-id) + default-team-id (:id default-team) + result (nitrate/call tx-cfg :add-profile-to-org {:profile-id profile-id + :team-id default-team-id + :org-id org-id})] + (when (not (:is-member result)) + (ex/raise :type :internal + :code :failed-add-profile-org-nitrate + :context {:profile-id profile-id + :team-id team-id + :org-id org-id + :default-team-id default-team-id})) + nil)))))) + +(defn add-profile-to-team! + ([cfg params] + (add-profile-to-team! cfg params nil)) + ([{:keys [::db/conn] :as cfg} {:keys [:profile-id :team-id] :as params} options] + (assert (db/connection-map? cfg) + "expected cfg with valid connection") + (when (contains? cf/flags :nitrate) + (initialize-user-in-nitrate-org cfg profile-id team-id)) + (db/insert! conn :team-profile-rel params options))) + (defn create-team "This is a complete team creation process, it creates the team object and all related objects (default role and default project)." - [cfg-or-conn params] - (let [conn (db/get-connection cfg-or-conn) - team (create-team* conn params) + [{:keys [::db/conn] :as cfg} params] + (assert (db/connection-map? cfg) + "expected cfg with valid connection") + (let [team (create-team* conn params) params (assoc params :team-id (:id team) :role :owner) project (create-team-default-project conn params)] - (create-team-role conn params) + (create-team-role cfg params) ;; Set team organization in Nitrate if organization-id is provided (when (and (contains? cf/flags :nitrate) (:organization-id params)) - (nitrate/set-team-organization cfg-or-conn team params)) + (nitrate/set-team-organization cfg team params)) (assoc team :default-project-id (:id project)))) (defn- create-team* @@ -551,11 +609,13 @@ (decode-row team))) (defn- create-team-role - [conn {:keys [profile-id team-id role] :as params}] + [cfg {:keys [profile-id team-id role] :as params}] + (assert (db/connection-map? cfg) + "expected cfg with valid connection") (let [params {:team-id team-id :profile-id profile-id}] (->> (perms/assign-role-flags params role) - (db/insert! conn :team-profile-rel)))) + (add-profile-to-team! cfg)))) (defn- create-team-default-project [conn {:keys [profile-id team-id] :as params}] diff --git a/backend/src/app/rpc/commands/teams_invitations.clj b/backend/src/app/rpc/commands/teams_invitations.clj index 5cffdd0c69..deed84912c 100644 --- a/backend/src/app/rpc/commands/teams_invitations.clj +++ b/backend/src/app/rpc/commands/teams_invitations.clj @@ -85,7 +85,8 @@ (defn- create-invitation [{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}] - (assert (db/connection? conn) "expected valid connection on cfg parameter") + (assert (db/connection-map? cfg) + "expected cfg with valid connection") (assert (check-create-invitation-params params)) (let [email (profile/clean-email email) @@ -104,8 +105,7 @@ (get types.team/permissions-for-role role))] ;; Insert the invited member to the team - (db/insert! conn :team-profile-rel params - {::db/on-conflict-do-nothing? true}) + (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true}) ;; If profile is not yet verified, mark it as verified because ;; accepting an invitation link serves as verification. @@ -166,7 +166,9 @@ itoken))))) (defn- add-member-to-team - [conn profile team role member] + [{:keys [::db/conn] :as cfg} profile team role member] + (assert (db/connection-map? cfg) + "expected cfg with valid connection") (let [team-id (:id team) params (merge @@ -186,7 +188,7 @@ ::quotes/team-id team-id}) ;; Insert the member to the team - (db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true}) + (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true}) ;; Delete any request (db/delete! conn :team-access-request @@ -268,7 +270,7 @@ (filter #(contains? invitation-emails (key %))) (map (fn [[email member]] (let [role (:role (first (filter #(= (:email %) email) invitation-data)))] - (add-member-to-team conn profile team role member)))) + (add-member-to-team cfg profile team role member)))) (doall)) invitations)) diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index a3454f7135..9f78ef3e12 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -18,6 +18,7 @@ [app.main :as-alias main] [app.rpc :as-alias rpc] [app.rpc.commands.profile :as profile] + [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] [app.rpc.quotes :as quotes] @@ -104,7 +105,7 @@ ::quotes/team-id team-id}) ;; Insert the invited member to the team - (db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true}) + (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true}) ;; If profile is not yet verified, mark it as verified because ;; accepting an invitation link serves as verification. diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index b6ac38a4d4..fd15ff811b 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -8,7 +8,6 @@ "Internal Nitrate HTTP RPC API. Provides authenticated access to organization management and token validation endpoints." (:require - [app.common.features :as cfeat] [app.common.schema :as sm] [app.common.types.profile :refer [schema:profile, schema:basic-profile]] [app.common.types.team :refer [schema:team]] @@ -21,9 +20,7 @@ [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] [app.rpc.doc :as doc] - [app.rpc.quotes :as quotes] [app.util.services :as sv] - [clojure.set :as set] [cuerdas.core :as str])) ;; ---- API: authenticate @@ -116,25 +113,15 @@ [:organization-id ::sm/uuid] [:role ::sm/text]]) + + (sv/defmethod ::notify-user-added-to-organization "Notify to Penpot that an user has joined an org from nitrate" {::doc/added "2.14" ::sm/params schema:notify-user-added-to-organization ::rpc/auth false} [cfg {:keys [profile-id organization-id]}] - (quotes/check! cfg {::quotes/id ::quotes/teams-per-profile - ::quotes/profile-id profile-id}) - - (let [features (-> (cfeat/get-enabled-features cf/flags) - (set/difference cfeat/frontend-only-features) - (set/difference cfeat/no-team-inheritable-features)) - params {:profile-id profile-id - :name "Default" - :features features - :organization-id organization-id - :is-default true} - team (db/tx-run! cfg teams/create-team params)] - (select-keys team [:id]))) + (db/tx-run! cfg teams/create-default-org-team profile-id organization-id)) ;; ---- API: get-managed-profiles diff --git a/backend/src/app/srepl/cli.clj b/backend/src/app/srepl/cli.clj index 519df65b6e..cec1ec6a97 100644 --- a/backend/src/app/srepl/cli.clj +++ b/backend/src/app/srepl/cli.clj @@ -53,7 +53,7 @@ :or {is-active true}}] (some-> (get-current-system) (db/tx-run! - (fn [{:keys [::db/conn] :as system}] + (fn [system] (let [password (derive-password password) params {:id (uuid/next) :email email @@ -62,7 +62,7 @@ :password password :props {}}] (->> (cmd.auth/create-profile system params) - (cmd.auth/create-profile-rels conn))))))) + (cmd.auth/create-profile-rels system))))))) (defmethod exec-command "update-profile" [{:keys [fullname email password is-active]}] diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj index 30c8b403dc..f25bec50cb 100644 --- a/backend/src/app/srepl/main.clj +++ b/backend/src/app/srepl/main.clj @@ -905,5 +905,4 @@ (let [params (-> rel (assoc :id (uuid/next)) (assoc :team-id (:id team)))] - (db/insert! conn :team-profile-rel params - {::db/return-keys false})))))))) + (teams/add-profile-to-team! cfg params {::db/return-keys false})))))))) diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index 8ddb3448a2..081af944e3 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -186,10 +186,10 @@ :is-demo false} params)] (db/run! system - (fn [{:keys [::db/conn] :as cfg}] + (fn [cfg] (->> params (cmd.auth/create-profile cfg) - (cmd.auth/create-profile-rels conn))))))) + (cmd.auth/create-profile-rels cfg))))))) (defn create-project* ([i params] (create-project* *system* i params)) @@ -234,10 +234,10 @@ (dm/with-open [conn (db/open system)] (let [id (mk-uuid "team" i) features (cfeat/get-enabled-features cf/flags)] - (teams/create-team conn {:id id - :profile-id profile-id - :features features - :name (str "team" i)}))))) + (teams/create-team {::db/conn conn} {:id id + :profile-id profile-id + :features features + :name (str "team" i)}))))) (defn create-file-media-object* ([params] (create-file-media-object* *system* params)) @@ -283,9 +283,10 @@ ([params] (create-team-role* *system* params)) ([system {:keys [team-id profile-id role] :or {role :owner}}] (dm/with-open [conn (db/open system)] - (#'teams/create-team-role conn {:team-id team-id - :profile-id profile-id - :role role})))) + (#'teams/create-team-role {::db/conn conn} + {:team-id team-id + :profile-id profile-id + :role role})))) (defn create-project-role* ([params] (create-project-role* *system* params))