Add create default team org for nitrate on adding an user to a team

This commit is contained in:
Pablo Alba 2026-03-24 14:53:29 +01:00 committed by Pablo Alba
parent 74af101462
commit 1af2521f64
12 changed files with 147 additions and 63 deletions

View File

@ -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 {}))

View File

@ -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?)

View File

@ -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)})))

View File

@ -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))))))

View File

@ -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]

View File

@ -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}]

View File

@ -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))

View File

@ -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.

View File

@ -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

View File

@ -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]}]

View File

@ -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}))))))))

View File

@ -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))