diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index 77307a1482..114f07be3e 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -166,8 +166,12 @@ ;; the same row/object. (let [profile (get-profile conn profile-id ::db/for-update true) fullname (d/normalize-string fullname) - lang (d/normalize-string lang) - theme (d/normalize-string theme) + lang (if (contains? params :lang) + (d/normalize-string lang) + (:lang profile)) + theme (if (contains? params :theme) + (d/normalize-string theme) + (:theme profile)) ;; Update the profile map with direct params profile (-> profile (assoc :fullname fullname) diff --git a/backend/src/app/rpc/rlimit.clj b/backend/src/app/rpc/rlimit.clj index abc77d81ea..3e05eb9837 100644 --- a/backend/src/app/rpc/rlimit.clj +++ b/backend/src/app/rpc/rlimit.clj @@ -46,6 +46,7 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.logging :as l] + [app.common.math :as mth] [app.common.schema :as sm] [app.common.time :as ct] [app.common.uri :as uri] @@ -180,8 +181,8 @@ result (rds/eval rconn script) allowed? (boolean (nth result 0)) remaining (nth result 1) - reset (* (/ (inst-ms interval) rate) - (- capacity remaining))] + reset (long (mth/ceil (double (* (/ (inst-ms interval) rate) + (- capacity remaining)))))] (l/trace :hint "limit processed" :method method :limit (name (::name limit)) diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index f9900d44ab..de9ae0aacd 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -125,6 +125,17 @@ (t/is (= "en" (:lang result))) (t/is (= "dark" (:theme result)))))) + (t/testing "update profile preserves omitted optional fields" + (let [data {::th/type :update-profile + ::rpc/profile-id (:id profile) + :fullname "Updated Name"} + out (th/command! data)] + + (t/is (nil? (:error out))) + (t/is (= "Updated Name" (get-in out [:result :fullname]))) + (t/is (= "en" (get-in out [:result :lang]))) + (t/is (= "dark" (get-in out [:result :theme]))))) + (t/testing "update photo" (let [data {::th/type :update-profile-photo ::rpc/profile-id (:id profile) diff --git a/backend/test/backend_tests/rpc_rlimit_test.clj b/backend/test/backend_tests/rpc_rlimit_test.clj new file mode 100644 index 0000000000..87ef58244f --- /dev/null +++ b/backend/test/backend_tests/rpc_rlimit_test.clj @@ -0,0 +1,28 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC Sucursal en Espana SL + +(ns backend-tests.rpc-rlimit-test + (:require + [app.common.time :as ct] + [app.redis :as rds] + [app.rpc.rlimit :as rlimit] + [clojure.test :as t])) + +(t/deftest bucket-reset-supports-fractional-milliseconds + (let [now (ct/inst 0) + limit {::rlimit/name :test + ::rlimit/strategy :bucket + ::rlimit/key "test" + ::rlimit/method "main.test" + ::rlimit/capacity 5 + ::rlimit/rate 3 + ::rlimit/interval (ct/duration 1000) + ::rlimit/params [1 3 5] + ::rlimit/opts "5/3/1s"}] + (with-redefs [rds/eval (fn [_ _] [true 4])] + (let [result (rlimit/process-limit nil "profile" now limit)] + (t/is (= (ct/inst 334) + (:app.rpc.rlimit.result/reset result))))))) diff --git a/docker/images/files/nginx.conf.template b/docker/images/files/nginx.conf.template index 5ca929c124..75a385f9d6 100644 --- a/docker/images/files/nginx.conf.template +++ b/docker/images/files/nginx.conf.template @@ -161,6 +161,16 @@ http { location / { include /etc/nginx/overrides/location.d/*.conf; + # Regenerated from the environment on every container start + # (see nginx-entrypoint.sh) while its URL is only versioned by + # the build, so a flags only restart leaves the URL untouched. + # Caching it like a build asset would keep returning users on + # the previous PENPOT_FLAGS for up to a week. + location = /js/config.js { + include /etc/nginx/nginx-security-headers.conf; + add_header Cache-Control "no-store, no-cache, max-age=0" always; + } + location ~* \.(js|css|jpg|png|svg|gif|ttf|woff|woff2|wasm|map)$ { include /etc/nginx/nginx-security-headers.conf; add_header Cache-Control "public, max-age=604800" always; # 7 days diff --git a/frontend/src/app/main/data/profile.cljs b/frontend/src/app/main/data/profile.cljs index 3f1237c2b7..9d6db2101d 100644 --- a/frontend/src/app/main/data/profile.cljs +++ b/frontend/src/app/main/data/profile.cljs @@ -122,6 +122,10 @@ ;; --- Update Profile +(defn profile-update-params + [profile] + (d/without-nils (select-keys profile [:fullname :lang :theme]))) + (defn persist-profile [& {:as opts}] (ptk/reify ::persist-profile @@ -130,7 +134,7 @@ (let [on-success (:on-success opts identity) on-error (:on-error opts rx/throw) profile (:profile state) - params (select-keys profile [:fullname :lang :theme])] + params (profile-update-params profile)] (->> (rp/cmd! :update-profile params) (rx/tap on-success) (rx/map set-profile) @@ -143,7 +147,7 @@ props" [profile] - (let [profile (check-profile profile)] + (let [profile (check-profile (d/without-nils profile))] (ptk/reify ::update-profile ptk/WatchEvent (watch [_ state _] diff --git a/frontend/src/app/main/data/team.cljs b/frontend/src/app/main/data/team.cljs index 74b9a97bc9..d250543807 100644 --- a/frontend/src/app/main/data/team.cljs +++ b/frontend/src/app/main/data/team.cljs @@ -609,50 +609,56 @@ (defn check-and-submit-invite-members "Fetches fresh team data from the server to ensure up-to-date organization - permissions, then submits member invitations or shows a restriction modal." + permissions, then submits member invitations or shows a permission error." [{:keys [team-id] :as params} origin do-invite-members] (ptk/reify ::check-and-submit-invite-members ptk/WatchEvent - (watch [_ _ _] - (if (contains? cf/flags :admin-console) - (with-refreshed-team team-id - (fn [team] - (if (not (cto/allowed? :add-anybody-to-team - {:organization-perms (:organization team)})) - (->> (rp/cmd! :check-organization-members {:organization-id (get-in team [:organization :id]) - :emails (vec (:emails params))}) - (rx/mapcat - (fn [result] - (let [blocked (into [] (comp (filter (fn [[_ v]] (not v))) - (map first)) - result)] - (cond - (empty? blocked) - (do (do-invite-members params origin) (rx/empty)) + (watch [_ state _] + (let [profile-id (dm/get-in state [:profile :id])] + (if (contains? cf/flags :admin-console) + (with-refreshed-team team-id + (fn [team] + (if (not (cto/can-send-invitations? + {:organization (:organization team) + :profile-id profile-id + :team-permissions (:permissions team)})) + (rx/of (modal/show :no-permission-modal {:type :invite-members})) + (if (not (cto/allowed? :add-anybody-to-team + {:organization-perms (:organization team)})) + (->> (rp/cmd! :check-organization-members {:organization-id (get-in team [:organization :id]) + :emails (vec (:emails params))}) + (rx/mapcat + (fn [result] + (let [blocked (into [] (comp (filter (fn [[_ v]] (not v))) + (map first)) + result)] + (cond + (empty? blocked) + (do (do-invite-members params origin) (rx/empty)) - (= (count blocked) (count result)) - (rx/of - (modal/show - {:type :alert - :title (tr "modals.invite-restricted-members.all-blocked-title") - :message (tr "modals.invite-restricted-members.all-blocked") - :accept-label (tr "labels.accept") - :accept-style :primary})) + (= (count blocked) (count result)) + (rx/of + (modal/show + {:type :alert + :title (tr "modals.invite-restricted-members.all-blocked-title") + :message (tr "modals.invite-restricted-members.all-blocked") + :accept-label (tr "labels.accept") + :accept-style :primary})) - :else - (rx/of - (modal/show - {:type :invite-restricted-members - :blocked-emails blocked - :on-accept (fn [] - (let [valid-emails (into #{} (filter (fn [e] (get result e))) - (:emails params)) - params' (assoc params :emails valid-emails)] - (do-invite-members params' origin)))}))))))) - (do (do-invite-members params origin) - (rx/empty))))) - (do (do-invite-members params origin) - (rx/empty)))))) + :else + (rx/of + (modal/show + {:type :invite-restricted-members + :blocked-emails blocked + :on-accept (fn [] + (let [valid-emails (into #{} (filter (fn [e] (get result e))) + (:emails params)) + params' (assoc params :emails valid-emails)] + (do-invite-members params' origin)))}))))))) + (do (do-invite-members params origin) + (rx/empty)))))) + (do (do-invite-members params origin) + (rx/empty))))))) (defn copy-invitation-link [{:keys [email team-id] :as params}] diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index d9f8071981..027238e4b8 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -233,6 +233,9 @@ (= :email-domain-is-not-allowed code)) (st/emit! (ntf/error (tr "errors.email-domain-not-allowed")) (modal/hide)) + (and (= :validation type) + (= :insufficient-permissions code)) + (st/emit! (modal/show :no-permission-modal {:type :invite-members})) :else (st/emit! (ntf/error (tr "errors.generic")) diff --git a/frontend/src/app/main/ui/dashboard/team_form.cljs b/frontend/src/app/main/ui/dashboard/team_form.cljs index 21a9708c4d..d69c108183 100644 --- a/frontend/src/app/main/ui/dashboard/team_form.cljs +++ b/frontend/src/app/main/ui/dashboard/team_form.cljs @@ -152,6 +152,8 @@ (tr "dashboard.no-permission-create-team.message" organization-name)] :delete-team [(tr "dashboard.delete-team") (tr "dashboard.no-permission-delete-team.message" organization-name)] + :invite-members [(tr "modals.invite-team-member.title") + (tr "dashboard.invitations.no-permission")] :no-organizations-create [(tr "dashboard.select-organization-modal.title") (tr "dashboard.no-organization-allows-create-team.message")] :no-organizations-change [(tr "dashboard.change-organization-modal.title") diff --git a/frontend/test/frontend_tests/data/profile_test.cljs b/frontend/test/frontend_tests/data/profile_test.cljs new file mode 100644 index 0000000000..97e62f63d6 --- /dev/null +++ b/frontend/test/frontend_tests/data/profile_test.cljs @@ -0,0 +1,30 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC Sucursal en EspaƱa SL + +(ns frontend-tests.data.profile-test + (:require + [app.common.uuid :as uuid] + [app.main.data.profile :as dprof] + [cljs.test :as t :include-macros true])) + +(t/deftest profile-update-params-omits-nil-values + (t/is (= {:fullname "Updated Name"} + (dprof/profile-update-params {:fullname "Updated Name" + :lang nil + :theme nil})))) + +(t/deftest profile-update-params-preserves-present-values + (t/is (= {:fullname "Updated Name" + :lang "en" + :theme "dark"} + (dprof/profile-update-params {:fullname "Updated Name" + :lang "en" + :theme "dark"})))) + +(t/deftest update-profile-accepts-nil-optional-values + (t/is (some? (dprof/update-profile {:id uuid/zero + :fullname "Updated Name" + :theme nil})))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 6cc1faff3e..5b7ecdbeed 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -11,6 +11,7 @@ [frontend-tests.data.dashboard-test] [frontend-tests.data.exports-assets-test] [frontend-tests.data.nitrate-test] + [frontend-tests.data.profile-test] [frontend-tests.data.repo-test] [frontend-tests.data.store-test] [frontend-tests.data.uploads-test] @@ -107,6 +108,7 @@ 'frontend-tests.copy-as-svg-test 'frontend-tests.data.dashboard-test 'frontend-tests.data.nitrate-test + 'frontend-tests.data.profile-test 'frontend-tests.data.repo-test 'frontend-tests.data.store-test 'frontend-tests.data.exports-assets-test