From 4ddabaebff67e0a95dda4928e956f70adbbdd449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Valderrama?= Date: Wed, 6 May 2026 10:13:17 +0200 Subject: [PATCH] :sparkles: Add Nitrate's advanced permissions * :sparkles: Add Nitrate's advanced permissions * :paperclip: Code review --- backend/src/app/nitrate.clj | 15 ++++ backend/src/app/rpc/commands/nitrate.clj | 15 ++++ backend/src/app/rpc/commands/teams.clj | 18 ++++- common/src/app/common/types/organization.cljc | 8 +- frontend/src/app/main/data/nitrate.cljs | 78 +++++++++++++++++++ frontend/src/app/main/data/team.cljs | 31 +++++++- .../src/app/main/ui/dashboard/sidebar.cljs | 7 +- frontend/src/app/main/ui/dashboard/team.cljs | 64 +++++++-------- frontend/src/app/main/ui/dashboard/team.scss | 7 ++ .../src/app/main/ui/dashboard/team_form.cljs | 63 +++++++++++---- .../src/app/main/ui/dashboard/team_form.scss | 40 +++++----- frontend/translations/en.po | 11 ++- frontend/translations/es.po | 11 ++- 13 files changed, 285 insertions(+), 83 deletions(-) diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index 28f2cf1eb7..77935cbad8 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -348,6 +348,20 @@ [:map [:cancel-at [:maybe schema:timestamp]]]) +(defn- get-org-permissions-api + [cfg {:keys [organization-id] :as params}] + (let [baseuri (cf/get :nitrate-backend-uri)] + (request-to-nitrate cfg :get + (str baseuri + "/api/organizations/" + organization-id + "/permissions") + [:map + [:organization-id ::sm/uuid] + [:owner-id ::sm/uuid] + [:create-teams [:enum "any" "onlyMe"]]] + params))) + (defn- redeem-activation-code-api [cfg params] (let [baseuri (cf/get :nitrate-backend-uri)] @@ -372,6 +386,7 @@ :add-profile-to-org (partial add-profile-to-org-api cfg) :remove-profile-from-org (partial remove-profile-from-org-api cfg) :remove-profile-from-all-orgs (partial remove-profile-from-all-orgs-api cfg) + :get-org-permissions (partial get-org-permissions-api cfg) :delete-team (partial delete-team-api cfg) :remove-team-from-org (partial remove-team-from-org-api cfg) :get-subscription (partial get-subscription-api cfg) diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index 91f58f6448..cc77878edb 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -12,6 +12,7 @@ [app.common.exceptions :as ex] [app.common.schema :as sm] [app.common.time :as ct] + [app.config :as cf] [app.db :as db] [app.nitrate :as nitrate] [app.rpc :as-alias rpc] @@ -305,6 +306,20 @@ (assert-not-default-team cfg team-id) (assert-membership cfg profile-id organization-id) + (when (contains? cf/flags :nitrate) + (let [org-perms (nitrate/call cfg :get-org-permissions + {:organization-id organization-id})] + (if (nil? org-perms) + (ex/raise :type :validation + :code :not-allowed + :hint "Unable to verify organization permissions") + (let [create-perm (:create-teams org-perms) + is-owner? (= profile-id (:owner-id org-perms))] + (when (and (= create-perm "onlyMe") (not is-owner?)) + (ex/raise :type :validation + :code :not-allowed + :hint "You are not allowed to add teams in this organization")))))) + (let [team-members (db/query cfg :team-profile-rel {:team-id team-id})] ;; Add teammates to the org if needed (doseq [{member-id :profile-id} team-members diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 359b79c841..7c84657086 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -506,11 +506,27 @@ (sv/defmethod ::create-team {::doc/added "1.17" ::sm/params schema:create-team} - [cfg {:keys [::rpc/profile-id] :as params}] + [cfg {:keys [::rpc/profile-id organization-id] :as params}] (quotes/check! cfg {::quotes/id ::quotes/teams-per-profile ::quotes/profile-id profile-id}) + ;; When creating inside an org, verify the user has permission to do so. + ;; Fail closed: if org permissions cannot be fetched, deny the operation. + (when (and organization-id (contains? cf/flags :nitrate)) + (let [org-perms (nitrate/call cfg :get-org-permissions + {:organization-id organization-id})] + (if (nil? org-perms) + (ex/raise :type :validation + :code :not-allowed + :hint "Unable to verify organization permissions") + (let [create-perm (:create-teams org-perms) + is-owner? (= profile-id (:owner-id org-perms))] + (when (and (= create-perm "onlyMe") (not is-owner?)) + (ex/raise :type :validation + :code :not-allowed + :hint "You are not allowed to create teams in this organization")))))) + (let [features (-> (cfeat/get-enabled-features cf/flags) (set/difference cfeat/frontend-only-features) (set/difference cfeat/no-team-inheritable-features)) diff --git a/common/src/app/common/types/organization.cljc b/common/src/app/common/types/organization.cljc index f19833b585..9205da9246 100644 --- a/common/src/app/common/types/organization.cljc +++ b/common/src/app/common/types/organization.cljc @@ -15,7 +15,8 @@ [:slug ::sm/text] [:owner-id ::sm/uuid] [:avatar-bg-url ::sm/uri] - [:logo-id {:optional true} [:maybe ::sm/uuid]]]) + [:logo-id {:optional true} [:maybe ::sm/uuid]] + [:create-teams {:optional true} [:maybe [:enum "any" "onlyMe"]]]]) (def schema:team-with-organization @@ -31,7 +32,8 @@ [:custom-photo :organization-custom-photo] [:slug :organization-slug] [:avatar-bg-url :organization-avatar-bg-url] - [:owner-id :organization-owner-id]]) + [:owner-id :organization-owner-id] + [:create-teams :organization-create-teams]]) (defn apply-organization "Updates a team map with organization fields sourced from org. @@ -47,4 +49,4 @@ (assoc acc team-k v) (dissoc acc team-k)))) team - organization->team-keys))) \ No newline at end of file + organization->team-keys))) diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs index 77374a0a0f..1748ae516c 100644 --- a/frontend/src/app/main/data/nitrate.cljs +++ b/frontend/src/app/main/data/nitrate.cljs @@ -134,3 +134,81 @@ (rx/mapcat (fn [_] (rx/of (modal/hide)))))))) + +(defn show-add-team-to-org-modal + "Fetches fresh team/org data, then shows the add-to-org modal + restricted to orgs where the user has permission, or the no-permission + modal if none qualify." + [{:keys [team-id]}] + (ptk/reify ::show-add-team-to-org-modal + ptk/WatchEvent + (watch [_ state _] + (let [profile-id (dm/get-in state [:profile :id])] + (->> (rp/cmd! :get-teams) + (rx/mapcat + (fn [teams] + (let [all-orgs (map dt/team->organization + (filter #(and (:is-default %) (:organization-id %)) teams)) + orgs (filter (fn [org] + (let [perm (:create-teams org) + is-own? (= profile-id (:owner-id org))] + (or (= perm "any") is-own?))) all-orgs) + team (first (filter #(= (:id %) team-id) teams)) + on-confirm (fn [organization-id] + (st/emit! (add-team-to-org {:team-id team-id + :organization-id organization-id})))] + (rx/of (dt/teams-fetched teams) + (if (empty? orgs) + (modal/show :no-org-allows-create-team {}) + (let [has-filtered? (< (count orgs) (count all-orgs)) + extra-props (when has-filtered? + {:info-message-key "dashboard.select-org-modal.permission-info"})] + (modal/show :select-organization-modal + (merge {:organizations orgs + :current-organization-id (:organization-id team) + :on-confirm on-confirm + :title-key "dashboard.select-org-modal.title" + :choose-key "dashboard.select-org-modal.choose" + :placeholder-key "dashboard.select-org-modal.select" + :accept-key "dashboard.select-org-modal.accept" + :cancel-key "labels.cancel"} + extra-props))))))))))))) + +(defn show-change-team-org-modal + "Fetches fresh team/org data, then shows the change-org modal + restricted to orgs where the user has permission, or the no-permission + modal if none qualify." + [{:keys [team-id]}] + (ptk/reify ::show-change-team-org-modal + ptk/WatchEvent + (watch [_ state _] + (let [profile-id (dm/get-in state [:profile :id])] + (->> (rp/cmd! :get-teams) + (rx/mapcat + (fn [teams] + (let [all-orgs (map dt/team->organization + (filter #(and (:is-default %) (:organization-id %)) teams)) + orgs (filter (fn [org] + (let [perm (:create-teams org) + is-own? (= profile-id (:owner-id org))] + (or (= perm "any") is-own?))) all-orgs) + team (first (filter #(= (:id %) team-id) teams)) + on-confirm (fn [organization-id] + (st/emit! (add-team-to-org {:team-id team-id + :organization-id organization-id})))] + (rx/of (dt/teams-fetched teams) + (if (empty? orgs) + (modal/show :no-org-allows-create-team {}) + (let [has-filtered? (< (count orgs) (count all-orgs)) + extra-props (when has-filtered? + {:info-message-key "dashboard.select-org-modal.permission-info"})] + (modal/show :select-organization-modal + (merge {:organizations orgs + :current-organization-id (:organization-id team) + :on-confirm on-confirm + :title-key "dashboard.change-org-modal.title" + :choose-key "dashboard.change-org-modal.choose" + :placeholder-key "dashboard.change-org-modal.select" + :accept-key "dashboard.change-org-modal.accept" + :cancel-key "labels.cancel"} + extra-props))))))))))))) diff --git a/frontend/src/app/main/data/team.cljs b/frontend/src/app/main/data/team.cljs index f3bb5207bf..337e853ba6 100644 --- a/frontend/src/app/main/data/team.cljs +++ b/frontend/src/app/main/data/team.cljs @@ -16,6 +16,7 @@ [app.config :as cf] [app.main.data.event :as ev] [app.main.data.media :as di] + [app.main.data.modal :as modal] [app.main.data.profile :as dp] [app.main.features :as features] [app.main.repo :as rp] @@ -50,7 +51,8 @@ :organization-name :organization-slug :organization-owner-id - :organization-avatar-bg-url))] + :organization-avatar-bg-url + :organization-create-teams))] (update state :teams assoc id team-updated))) state teams))))) @@ -63,6 +65,30 @@ (->> (rp/cmd! :get-teams) (rx/map teams-fetched))))) +(defn check-and-create-team + "Fetches fresh team data from the server to ensure up-to-date org + permissions, then shows the team-form modal or a no-permission modal." + [team-id] + (ptk/reify ::check-and-create-team + ptk/WatchEvent + (watch [_ state _] + (let [profile-id (dm/get-in state [:profile :id])] + (->> (rp/cmd! :get-teams) + (rx/mapcat + (fn [teams] + (let [team (d/seek #(= (:id %) team-id) teams) + in-org? (and (contains? cf/flags :nitrate) (:organization-id team)) + create-perm (:organization-create-teams team) + is-owner? (= profile-id (:organization-owner-id team)) + can-create? (or (not in-org?) (= create-perm "any") is-owner?)] + (rx/of (teams-fetched teams) + (if can-create? + (modal/show :team-form (if in-org? + {:organization-id (:organization-id team) + :organization-name (:organization-name team)} + {})) + (modal/show :no-permission-create-team {:organization-name (:organization-name team)}))))))))))) + ;; --- EVENT: fetch-members (defn- members-fetched @@ -598,5 +624,6 @@ :avatar-bg-url (:organization-avatar-bg-url team) :custom-photo (:organization-custom-photo team) :name (:organization-name team) - :default-team-id (:id team)}) + :default-team-id (:id team) + :create-teams (:organization-create-teams team)}) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 84509976a4..e7bd42ba53 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -384,10 +384,9 @@ (mf/use-fn (mf/deps team) (fn [] - (let [params (if (and (contains? cf/flags :nitrate) (:organization-id team)) - {:organization-id (:organization-id team)} - {})] - (st/emit! (modal/show :team-form params))))) + (if (contains? cf/flags :nitrate) + (st/emit! (dtm/check-and-create-team (:id team))) + (st/emit! (modal/show :team-form {}))))) on-team-click (mf/use-fn diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index dd3bbfb2de..3425c5cb4c 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -810,7 +810,7 @@ (mf/defc select-organization-modal {::mf/register modal/components ::mf/register-as :select-organization-modal} - [{:keys [organizations current-organization-id on-confirm title-key text-key choose-key placeholder-key accept-key cancel-key]}] + [{:keys [organizations current-organization-id on-confirm title-key text-key choose-key placeholder-key accept-key cancel-key info-message-key]}] (let [valid-organizations (mf/with-memo [organizations] (remove #(= (:id %) current-organization-id) organizations)) options (mf/with-memo [valid-organizations] @@ -848,6 +848,9 @@ [:div {:class (stl/css :modal-content :modal-select-org-text)} (tr text-key)]) [:div + (when info-message-key + [:div {:class (stl/css :modal-select-org-info)} + (tr info-message-key)]) [:div {:class (stl/css :modal-select-org-content)} (tr choose-key)] [:> combobox* {:id "selected-id" @@ -1366,18 +1369,29 @@ can-edit (or (:is-owner permissions) (:is-admin permissions)) - organizations (mf/deref refs/teams) - organizations (mf/with-memo [organizations] - (->> (vals organizations) - (filter :is-default) - (filter :organization-id) - (map dtm/team->organization))) + profile (mf/deref refs/profile) + profile-id (:id profile) + + all-organizations (mf/deref refs/teams) + all-organizations (mf/with-memo [all-organizations] + (->> (vals all-organizations) + (filter :is-default) + (filter :organization-id) + (map dtm/team->organization))) + + ;; Filter to orgs where user is allowed to create/add teams + organizations (mf/with-memo [all-organizations profile-id] + (->> all-organizations + (filter (fn [org] + (let [perm (:create-teams org) + is-owner? (= profile-id (:owner-id org))] + (or (= perm "any") is-owner?)))))) can-change-organization? (mf/with-memo [organizations] (> (count organizations) 1)) - can-add-to-organization? (mf/with-memo [organizations] - (and (pos? (count organizations)) + can-add-to-organization? (mf/with-memo [organizations all-organizations] + (and (pos? (count all-organizations)) (not (:is-default team)))) show-org-options-menu* @@ -1424,41 +1438,17 @@ :accept-style :danger}] (st/emit! (modal/show params))))) - on-add-team-to-org-confirm - (mf/use-fn - (mf/deps team) - (fn [organization-id] - (let [organization (d/seek #(= organization-id (:id %)) organizations)] - (when organization - (st/emit! (dnt/add-team-to-org {:team-id (:id team) - :organization-id organization-id})))))) - on-add-team-to-org (mf/use-fn - (mf/deps organizations on-add-team-to-org-confirm) + (mf/deps team) (fn [] - (st/emit! (modal/show :select-organization-modal {:organizations organizations - :current-organization-id (:organization-id team) - :on-confirm on-add-team-to-org-confirm - :title-key "dashboard.select-org-modal.title" - :choose-key "dashboard.select-org-modal.choose" - :placeholder-key "dashboard.select-org-modal.select" - :accept-key "dashboard.select-org-modal.accept" - :cancel-key "labels.cancel"})))) + (st/emit! (dnt/show-add-team-to-org-modal {:team-id (:id team)})))) on-change-team-org (mf/use-fn - (mf/deps organizations on-add-team-to-org-confirm) + (mf/deps team) (fn [] - (st/emit! (modal/show :select-organization-modal {:organizations organizations - :current-organization-id (:organization-id team) - :on-confirm on-add-team-to-org-confirm - :title-key "dashboard.change-org-modal.title" - :text-key "dashboard.change-org-modal.text" - :choose-key "dashboard.change-org-modal.choose" - :placeholder-key "dashboard.change-org-modal.select" - :accept-key "dashboard.change-org-modal.accept" - :cancel-key "labels.cancel"}))))] + (st/emit! (dnt/show-change-team-org-modal {:team-id (:id team)}))))] (mf/with-effect [team] (dom/set-html-title (tr "title.team-settings" diff --git a/frontend/src/app/main/ui/dashboard/team.scss b/frontend/src/app/main/ui/dashboard/team.scss index d68bbd26c9..5fe7d4ba21 100644 --- a/frontend/src/app/main/ui/dashboard/team.scss +++ b/frontend/src/app/main/ui/dashboard/team.scss @@ -887,6 +887,13 @@ margin-block-end: var(--sp-s); } +.modal-select-org-info { + @include t.use-typography("body-medium"); + + color: var(--color-foreground-secondary); + margin-block-end: var(--sp-xxxl); +} + .modal-select-org-title { @include t.use-typography("title-medium"); diff --git a/frontend/src/app/main/ui/dashboard/team_form.cljs b/frontend/src/app/main/ui/dashboard/team_form.cljs index 9a6240ce8d..603c680984 100644 --- a/frontend/src/app/main/ui/dashboard/team_form.cljs +++ b/frontend/src/app/main/ui/dashboard/team_form.cljs @@ -42,16 +42,19 @@ (modal/hide)))) (defn- on-error - [form _response] - (let [id (get-in @form [:clean-data :id])] - (if id - (rx/of (ntf/error "Error on updating team.")) - (rx/of (ntf/error "Error on creating team."))))) + [form organization-name response] + (let [id (get-in @form [:clean-data :id]) + code (-> response ex-data :code)] + (if (= code :not-allowed) + (rx/of (modal/show :no-permission-create-team {:organization-name organization-name})) + (if id + (rx/of (ntf/error "Error on updating team.")) + (rx/of (ntf/error "Error on creating team.")))))) (defn- on-create-submit - [form] + [form organization-name] (let [mdata {:on-success (partial on-create-success form) - :on-error (partial on-error form)} + :on-error (partial on-error form organization-name)} data (:clean-data @form) params (cond-> {:name (:name data)} (:organization-id data) (assoc :organization-id (:organization-id data)))] @@ -61,23 +64,23 @@ (defn- on-update-submit [form] (let [mdata {:on-success (partial on-update-success form) - :on-error (partial on-error form)} + :on-error (partial on-error form nil)} data (:clean-data @form) - team (select-keys data [:id :name])] ;; Only send name and id for updates + team (select-keys data [:id :name])] (st/emit! (dtm/update-team (with-meta team mdata)) (modal/hide)))) (defn- on-submit - [form _] + [organization-name form _] (let [data (:clean-data @form)] (if (:id data) (on-update-submit form) - (on-create-submit form)))) + (on-create-submit form organization-name)))) (mf/defc team-form-modal {::mf/register modal/components ::mf/register-as :team-form} - [{:keys [team organization-id] :as props}] + [{:keys [team organization-id organization-name] :as props}] (let [initial (mf/use-memo (mf/deps team organization-id) (fn [] @@ -89,18 +92,22 @@ organization-id (assoc :organization-id organization-id))))) form (fm/use-form :schema schema:team-form :initial initial) + on-submit* (mf/use-fn + (mf/deps organization-name) + (partial on-submit organization-name)) handle-keydown (mf/use-fn + (mf/deps organization-name) (fn [e] (when (kbd/enter? e) (dom/prevent-default e) (dom/stop-propagation e) - (on-submit form e))))] + (on-submit organization-name form e))))] [:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-container)} [:& fm/form {:form form - :on-submit on-submit + :on-submit on-submit* :class (stl/css :team-form)} [:div {:class (stl/css :modal-header)} @@ -132,3 +139,31 @@ :class (stl/css :accept-btn)}]]]]]])) +(mf/defc no-permission-create-team-modal* + {::mf/register modal/components + ::mf/register-as :no-permission-create-team} + [{:keys [organization-name]}] + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-header)} + [:h2 {:class (stl/css :modal-title)} + (tr "labels.create-team")] + [:button {:class (stl/css :modal-close-btn) + :on-click modal/hide!} deprecated-icon/close]] + [:div {:class (stl/css :modal-content)} + [:div (tr "dashboard.no-permission-create-team.message" organization-name)]]]]) + + +(mf/defc no-org-allows-create-team-modal* + {::mf/register modal/components + ::mf/register-as :no-org-allows-create-team} + [_props] + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-header)} + [:h2 {:class (stl/css :modal-title)} + (tr "dashboard.select-org-modal.title")] + [:button {:class (stl/css :modal-close-btn) + :on-click modal/hide!} deprecated-icon/close]] + [:div {:class (stl/css :modal-content)} + [:div (tr "dashboard.no-org-allows-create-team.message")]]]]) diff --git a/frontend/src/app/main/ui/dashboard/team_form.scss b/frontend/src/app/main/ui/dashboard/team_form.scss index 592ca7a94d..9b582dc574 100644 --- a/frontend/src/app/main/ui/dashboard/team_form.scss +++ b/frontend/src/app/main/ui/dashboard/team_form.scss @@ -5,6 +5,8 @@ // Copyright (c) KALEIDOS INC @use "refactor/common-refactor.scss" as deprecated; +@use "ds/_utils.scss" as *; +@use "ds/typography.scss" as t; .modal-overlay { @extend %modal-overlay-base; @@ -15,11 +17,11 @@ } .modal-header { - margin-bottom: deprecated.$s-24; + margin-block-end: var(--sp-xxl); } .modal-title { - @include deprecated.uppercase-title-typography; + @include t.use-typography("headline-large"); color: var(--modal-title-foreground-color); } @@ -29,33 +31,31 @@ } .modal-content { - margin-bottom: deprecated.$s-24; + margin-block-end: var(--sp-xxl); + color: var(--color-foreground-secondary); + + @include t.use-typography("body-large"); } .team-form { - min-width: deprecated.$s-400; + min-inline-size: px2rem(400); } .group-name-input { @extend %input-element-label; - @include deprecated.body-small-typography; - margin-bottom: deprecated.$s-8; + margin-block-end: var(--sp-s); +} - label { - @include deprecated.flex-column; - @include deprecated.body-small-typography; - - align-items: flex-start; - width: 100%; - border: none; - background-color: transparent; - height: 100%; - - input { - @include deprecated.body-small-typography; - } - } +.group-name-input label { + display: flex; + flex-direction: column; + gap: var(--sp-xs); + align-items: flex-start; + width: 100%; + border: none; + background-color: transparent; + height: 100%; } .action-buttons { diff --git a/frontend/translations/en.po b/frontend/translations/en.po index a38b24ba03..972e270a13 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -9395,4 +9395,13 @@ msgid "subscription.current-plan.nitrate-trial" msgstr "Nitrate Trial" msgid "subscription.current-plan.enterprise" -msgstr "Enterprise" \ No newline at end of file +msgstr "Enterprise" + +msgid "dashboard.no-permission-create-team.message" +msgstr "You are not allowed to create teams within %s organization. If you need more information, contact the owner." + +msgid "dashboard.no-org-allows-create-team.message" +msgstr "You don't have permission to add teams to any of your organizations." + +msgid "dashboard.select-org-modal.permission-info" +msgstr "Here you find all your organizations where you are allowed to create or add teams." diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 6a6814859c..e33fe69c17 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -9103,4 +9103,13 @@ msgid "subscription.current-plan.nitrate-trial" msgstr "Nitrate (Prueba)" msgid "subscription.current-plan.enterprise" -msgstr "Enterprise" \ No newline at end of file +msgstr "Enterprise" + +msgid "dashboard.no-permission-create-team.message" +msgstr "No tienes permiso para crear equipos en la organización %s. Si necesitas más información, contacta con el propietario." + +msgid "dashboard.no-org-allows-create-team.message" +msgstr "No tienes permiso para añadir equipos a ninguna de tus organizaciones." + +msgid "dashboard.select-org-modal.permission-info" +msgstr "Aquí encontrarás todas las organizaciones en las que tienes permiso para crear o añadir equipos."