diff --git a/frontend/src/app/main/data/team.cljs b/frontend/src/app/main/data/team.cljs index fe2b796ec8..80ab5e0fad 100644 --- a/frontend/src/app/main/data/team.cljs +++ b/frontend/src/app/main/data/team.cljs @@ -13,6 +13,7 @@ [app.common.types.organization :as cto] [app.common.types.team :as ctt] [app.common.uri :as u] + [app.common.uuid :as uuid] [app.config :as cf] [app.main.data.event :as ev] [app.main.data.helpers :as dsh] @@ -24,7 +25,9 @@ [app.main.repo :as rp] [app.main.router :as rt] [app.util.clipboard :as clipboard] + [app.util.dom :as dom] [app.util.i18n :refer [tr]] + [app.util.session-state :as ss] [app.util.storage :as storage] [beicon.v2.core :as rx] [clojure.string :as str] @@ -111,8 +114,8 @@ (f team))))))) (defn check-and-create-team - "Fetches fresh team data from the server to ensure up-to-date organization - permissions, then shows the team-form modal or a no-permission modal." + "Fetches fresh team data, checks organization permissions and SSO, then + shows the team form or the appropriate access error." [team-id] (ptk/reify ::check-and-create-team ptk/WatchEvent @@ -127,13 +130,44 @@ {:organization-perms organization :profile-id profile-id :team-perms (:permissions team)}) - true)] - (rx/of (if can-create? - (modal/show :team-form (if in-organization? - {:organization-id (:id organization) - :organization-name (:name organization)} - {})) - (modal/show :no-permission-modal {:type :create-team})))))))))) + true) + team-form-modal (modal/show :team-form (if in-organization? + {:organization-id (:id organization) + :organization-name (:name organization)} + {}))] + (cond + (not can-create?) + (rx/of (modal/show :no-permission-modal {:type :create-team})) + + (not in-organization?) + (rx/of team-form-modal) + + :else + (let [pending-id (str (uuid/next)) + callback-url (dom/append-query-param (rt/get-current-href) + :pending-action-id pending-id)] + (->> (rp/cmd! :check-nitrate-sso {:team-id team-id + :url callback-url}) + (rx/mapcat + (fn [{:keys [authorized redirect-uri]}] + (cond + authorized + (rx/of team-form-modal) + + redirect-uri + (do + (ss/save-pending-action! pending-id + {:type :create-team-in-organization + :team-id team-id}) + (rx/of (rt/nav-raw :uri (str redirect-uri)))) + + :else + (rx/of (rt/assign-exception + {:type :sso-error + :organization-id (:id organization) + :organization-name (:name organization) + :team-id team-id + :is-dashboard true}))))))))))))))) (defn check-and-delete-team "Fetches fresh team data from the server to ensure up-to-date organization diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index 13059c1036..43f43acbd0 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -18,6 +18,7 @@ [app.main.data.plugins :as dp] [app.main.data.profile :as dprof] [app.main.data.project :as dpj] + [app.main.data.team :as dtm] [app.main.refs :as refs] [app.main.router :as rt] [app.main.store :as st] @@ -298,6 +299,8 @@ (st/emit! (dnt/add-team-to-organization {:team-id (:team-id action) :organization-id (:organization-id action) :skip-audit? true})) + :create-team-in-organization + (st/emit! (dtm/check-and-create-team (:team-id action))) nil))))) (mf/defc dashboard* diff --git a/frontend/test/frontend_tests/data/team_test.cljs b/frontend/test/frontend_tests/data/team_test.cljs index 9bf84efc79..87ef76be68 100644 --- a/frontend/test/frontend_tests/data/team_test.cljs +++ b/frontend/test/frontend_tests/data/team_test.cljs @@ -5,9 +5,13 @@ (ns frontend-tests.data.team-test (:require [app.common.uuid :as uuid] + [app.main.data.modal :as modal] [app.main.data.team :as dtm] [app.main.repo :as rp] + [app.main.router :as rt] [app.main.ui.dashboard.team :as dtui] + [app.util.dom :as dom] + [app.util.session-state :as ss] [beicon.v2.core :as rx] [cljs.test :as t :include-macros true] [frontend-tests.helpers.async :as async] @@ -27,6 +31,89 @@ :on-next #(swap! emitted conj %)))) (.then (fn [_] {:team-id team-id :events @emitted}))))) +(t/deftest ^:async check-and-create-team-redirects-to-sso-and-remembers-the-action + (let [team-id "00000000-0000-4000-8000-000000000001" + organization-id "00000000-0000-4000-8000-000000000002" + profile-id "00000000-0000-4000-8000-000000000003" + dashboard-url "https://penpot.example/#/dashboard-recent?team-id=current-team" + callback-url (str dashboard-url "&pending-action-id=pending-id") + team {:id team-id + :organization {:id organization-id + :name "Product" + :owner-id profile-id}} + requests (atom []) + saved-actions (atom []) + emitted (atom [])] + (await + (mock/with-mocks* + {rp/cmd! (mock/stub + (fn [command params] + (swap! requests conj [command params]) + (case command + :get-teams (rx/of [team]) + :check-nitrate-sso + (rx/of {:authorized false + :redirect-uri "https://sso.example.test/login"})))) + rt/get-current-href (mock/stub (constantly dashboard-url)) + dom/append-query-param (mock/stub (fn [_url _key _value] callback-url)) + uuid/next (mock/stub (constantly "pending-id")) + ss/save-pending-action! (mock/stub (fn [& args] + (swap! saved-actions conj args))) + rt/nav-raw (fn [& args] {:nav-raw args}) + modal/show (mock/stub (fn [& args] {:modal-show args}))} + (await (async/observe + (ptk/watch (dtm/check-and-create-team team-id) + {:profile {:id profile-id}} + nil) + :on-next #(swap! emitted conj %))))) + + (t/is (= [[:get-teams nil] + [:check-nitrate-sso {:team-id team-id :url callback-url}]] + @requests)) + (t/is (= [["pending-id" {:type :create-team-in-organization + :team-id team-id}]] + @saved-actions)) + (t/is (= {:nav-raw [:uri "https://sso.example.test/login"]} + (last @emitted))) + (t/is (not-any? #(contains? % :modal-show) @emitted)))) + +(t/deftest ^:async check-and-create-team-shows-form-after-sso-is-authorized + (let [team-id "00000000-0000-4000-8000-000000000001" + organization-id "00000000-0000-4000-8000-000000000002" + profile-id "00000000-0000-4000-8000-000000000003" + dashboard-url "https://penpot.example/#/dashboard-recent?team-id=current-team" + callback-url (str dashboard-url "&pending-action-id=pending-id") + team {:id team-id + :organization {:id organization-id + :name "Product" + :owner-id profile-id}} + requests (atom []) + emitted (atom [])] + (await + (mock/with-mocks* + {rp/cmd! (mock/stub + (fn [command params] + (swap! requests conj [command params]) + (case command + :get-teams (rx/of [team]) + :check-nitrate-sso (rx/of {:authorized true})))) + rt/get-current-href (mock/stub (constantly dashboard-url)) + dom/append-query-param (mock/stub (fn [_url _key _value] callback-url)) + uuid/next (mock/stub (constantly "pending-id")) + modal/show (mock/stub (fn [& args] {:modal-show args}))} + (await (async/observe + (ptk/watch (dtm/check-and-create-team team-id) + {:profile {:id profile-id}} + nil) + :on-next #(swap! emitted conj %))))) + + (t/is (= [:check-nitrate-sso {:team-id team-id :url callback-url}] + (last @requests))) + (t/is (= {:modal-show [:team-form {:organization-id organization-id + :organization-name "Product"}]} + (last @emitted))) + (t/is (= 2 (count @emitted))))) + (t/deftest ^:async fetch-invitations-clears-the-list-on-permission-error ;; The pending invitations are an admin only list: a member that cannot ;; invite gets an insufficient permissions error. That must not become an