From fb76b547f30d6e2331ac1760b94f7ac95be09053 Mon Sep 17 00:00:00 2001 From: Anssi Kinnunen Date: Tue, 28 Jan 2025 15:16:14 +0200 Subject: [PATCH 01/80] test: verify basic db cache reloading Add rudimentary cache reload tests which verify that basic db entity caches initialize correctly from database: - rems.db.attachments - rems.db.blacklist - rems.db.catalogue - rems.db.form - rems.db.licenses - rems.db.organizations - rems.db.resource - rems.db.roles - rems.db.user-mappings - rems.db.user-settings (pre-existing) - rems.db.users - rems.db.workflow --- test/clj/rems/db/test_attachments.clj | 66 ++++++++++++++++++++ test/clj/rems/db/test_blacklist.clj | 61 ++++++++++++------ test/clj/rems/db/test_catalogue.clj | 57 ++++++++++++++++- test/clj/rems/db/test_form.clj | 42 +++++++++++++ test/clj/rems/db/test_licenses.clj | 47 ++++++++++++++ test/clj/rems/db/test_organizations.clj | 35 +++++++++++ test/clj/rems/db/test_resource.clj | 25 ++++++++ test/clj/rems/db/test_roles.clj | 12 ++++ test/clj/rems/db/test_user_mappings.clj | 62 ++++++++++++++++++ test/clj/rems/db/test_users.clj | 13 ++++ test/clj/rems/db/test_workflow.clj | 83 ++++++++++++++++--------- 11 files changed, 454 insertions(+), 49 deletions(-) create mode 100644 test/clj/rems/db/test_attachments.clj create mode 100644 test/clj/rems/db/test_form.clj create mode 100644 test/clj/rems/db/test_licenses.clj create mode 100644 test/clj/rems/db/test_organizations.clj create mode 100644 test/clj/rems/db/test_resource.clj create mode 100644 test/clj/rems/db/test_user_mappings.clj diff --git a/test/clj/rems/db/test_attachments.clj b/test/clj/rems/db/test_attachments.clj new file mode 100644 index 0000000000..0469a2ab34 --- /dev/null +++ b/test/clj/rems/db/test_attachments.clj @@ -0,0 +1,66 @@ +(ns ^:integration rems.db.test-attachments + (:require [clojure.test :refer [deftest is testing use-fixtures]] + [rems.cache :as cache] + [rems.db.attachments] + [rems.db.test-data-helpers :as test-helpers] + [rems.db.testing :refer [rollback-db-fixture test-db-fixture]] + [rems.util :refer [to-bytes]])) + +(use-fixtures :once test-db-fixture) +(use-fixtures :each rollback-db-fixture) + +(deftest test-attachments-cache + (testing "cache reload works" + (let [applicant (test-helpers/create-user! {:userid "alice"}) + cat-id (test-helpers/create-catalogue-item! {}) + app-id (test-helpers/create-application! {:catalogue-item-ids [cat-id] + :actor applicant}) + attachment-1 (test-helpers/create-attachment! {:application-id app-id + :actor applicant}) + attachment-2 (test-helpers/create-attachment! {:application-id app-id + :actor applicant})] + + ;; force cache reload + (cache/set-uninitialized! rems.db.attachments/attachment-cache) + + (is (= {attachment-1 {:application/id app-id + :attachment/id attachment-1 + :attachment/filename "attachment.pdf" + :attachment/type "application/pdf" + :attachment/user applicant} + attachment-2 {:application/id app-id + :attachment/id attachment-2 + :attachment/filename "attachment (1).pdf" + :attachment/type "application/pdf" + :attachment/user applicant}} + (into {} (cache/entries! rems.db.attachments/attachment-cache)))) + + (testing "dependent caches" + ;; verify that by-application-id is cached correctly (depends on attachment-cache) + (is (= {app-id [{:application/id app-id + :attachment/id attachment-1 + :attachment/filename "attachment.pdf" + :attachment/type "application/pdf" + :attachment/user applicant} + {:application/id app-id + :attachment/id attachment-2 + :attachment/filename "attachment (1).pdf" + :attachment/type "application/pdf" + :attachment/user applicant}]} + (into {} (cache/entries! @#'rems.db.attachments/by-application-id)))))))) + +(deftest test-license-attachments-cache + (testing "cache reload works" + (let [user-id (test-helpers/create-user! {:userid "alice"}) + attachment-id (rems.db.attachments/create-license-attachment! + {:user-id user-id + :filename "attachment.pdf" + :content-type "application/pdf" + :data (to-bytes "data")})] + (cache/set-uninitialized! rems.db.attachments/license-attachments-cache) + + (is (= {attachment-id {:attachment/id attachment-id + :attachment/user user-id + :attachment/filename "attachment.pdf" + :attachment/type "application/pdf"}} + (into {} (cache/entries! rems.db.attachments/license-attachments-cache))))))) diff --git a/test/clj/rems/db/test_blacklist.clj b/test/clj/rems/db/test_blacklist.clj index ad1530ae41..664fb31406 100644 --- a/test/clj/rems/db/test_blacklist.clj +++ b/test/clj/rems/db/test_blacklist.clj @@ -1,6 +1,7 @@ (ns ^:integration rems.db.test-blacklist (:require [clj-time.core :as time] [clojure.test :refer [deftest is testing use-fixtures]] + [rems.cache :as cache] [rems.db.blacklist] [rems.db.test-data-helpers :as test-helpers] [rems.db.testing :refer [test-db-fixture rollback-db-fixture]])) @@ -15,24 +16,48 @@ (test-helpers/create-resource! {:resource-ext-id "urn.fi/123"}) (test-helpers/create-resource! {:resource-ext-id "urn.fi/124"}) - (rems.db.blacklist/add-event! {:event/type :blacklist.event/add - :event/actor "handler" - :event/time (time/date-time 2019 1 2 8 0 0) - :userid "user1" - :resource/ext-id "urn.fi/123" - :event/comment "1"}) - (rems.db.blacklist/add-event! {:event/type :blacklist.event/remove - :event/actor "handler" - :event/time (time/date-time 2019 2 3 9 0 0) - :userid "user1" - :resource/ext-id "urn.fi/123" - :event/comment "2"}) - (rems.db.blacklist/add-event! {:event/type :blacklist.event/add - :event/actor "handler" - :event/time (time/date-time 2019 1 1 1 0 0) - :userid "user2" - :resource/ext-id "urn.fi/124" - :event/comment "3"}) + (let [event-1 (rems.db.blacklist/add-event! {:event/type :blacklist.event/add + :event/actor "handler" + :event/time (time/date-time 2019 1 2 8 0 0) + :userid "user1" + :resource/ext-id "urn.fi/123" + :event/comment "1"}) + event-2 (rems.db.blacklist/add-event! {:event/type :blacklist.event/remove + :event/actor "handler" + :event/time (time/date-time 2019 2 3 9 0 0) + :userid "user1" + :resource/ext-id "urn.fi/123" + :event/comment "2"}) + event-3 (rems.db.blacklist/add-event! {:event/type :blacklist.event/add + :event/actor "handler" + :event/time (time/date-time 2019 1 1 1 0 0) + :userid "user2" + :resource/ext-id "urn.fi/124" + :event/comment "3"})] + (testing "cache reload works" + (cache/set-uninitialized! rems.db.blacklist/blacklist-event-cache) + (is (= {event-1 {:event/id event-1 + :event/type :blacklist.event/add + :event/actor "handler" + :event/time (time/date-time 2019 1 2 8 0 0) + :userid "user1" + :resource/ext-id "urn.fi/123" + :event/comment "1"} + event-2 {:event/id event-2 + :event/type :blacklist.event/remove + :event/actor "handler" + :event/time (time/date-time 2019 2 3 9 0 0) + :userid "user1" + :resource/ext-id "urn.fi/123" + :event/comment "2"} + event-3 {:event/id event-3 + :event/type :blacklist.event/add + :event/actor "handler" + :event/time (time/date-time 2019 1 1 1 0 0) + :userid "user2" + :resource/ext-id "urn.fi/124" + :event/comment "3"}} + (into {} (cache/entries! rems.db.blacklist/blacklist-event-cache)))))) (let [events (rems.db.blacklist/get-events {:resource/ext-id "urn.fi/123"})] ;; event id sequence numbers aren't predictable since even diff --git a/test/clj/rems/db/test_catalogue.clj b/test/clj/rems/db/test_catalogue.clj index ccba04c618..39c1dc3419 100644 --- a/test/clj/rems/db/test_catalogue.clj +++ b/test/clj/rems/db/test_catalogue.clj @@ -1,8 +1,14 @@ -(ns rems.db.test-catalogue +(ns ^:integration rems.db.test-catalogue (:require [clj-time.core :as time] - [clojure.test :refer :all] + [clojure.test :refer [deftest is testing use-fixtures]] + [rems.cache :as cache] [rems.common.util :refer [apply-filters]] - [rems.db.catalogue])) + [rems.db.catalogue] + [rems.db.test-data-helpers :as test-helpers] + [rems.db.testing :refer [test-db-fixture rollback-db-fixture]])) + +(use-fixtures :once test-db-fixture) +(use-fixtures :each rollback-db-fixture) (deftest test-now-active? (let [t0 (time/epoch) @@ -75,3 +81,48 @@ (testing "calculates :active property" (is (every? #(contains? % :expired) (get-items {})))))) + +(deftest test-catalogue-cache + (testing "cache reload works" + (let [org-id (test-helpers/create-organization! {}) + form-id (test-helpers/create-form! {:form/external-title {:en "form" :fi "form" :sv "form"} + :form/internal-name "test-form"}) + workflow-id (test-helpers/create-workflow! {:title "workflow"}) + resource-id (test-helpers/create-resource! {:resource-ext-id "test-resource"}) + fixed-time (time/date-time 2020 1 1 12 0) + item-id (test-helpers/create-catalogue-item! {:resource-id resource-id + :form-id form-id + :workflow-id workflow-id + :organization {:organization/id org-id} + :title {:en "Test Item" + :fi "Testiasia"} + :infourl {:en "http://test.com" + :fi "http://test.fi"} + :start fixed-time})] + + ;; force cache reload + (cache/set-uninitialized! rems.db.catalogue/catalogue-item-cache) + (cache/set-uninitialized! rems.db.catalogue/catalogue-item-localizations-cache) + + (is (= {item-id {:archived false + :enabled true + :end nil + :formid form-id + :id item-id + :localizations {} ; joined from cache. see rems.db.catalogue/localize-catalogue-item + :organization {:organization/id org-id} + :resource-id resource-id + :start fixed-time + :wfid workflow-id}} + (into {} (cache/entries! rems.db.catalogue/catalogue-item-cache)))) + + (testing "localizations cache" + (is (= {item-id {:en {:id item-id + :infourl "http://test.com" + :langcode :en + :title "Test Item"} + :fi {:id item-id + :infourl "http://test.fi" + :langcode :fi + :title "Testiasia"}}} + (into {} (cache/entries! rems.db.catalogue/catalogue-item-localizations-cache)))))))) diff --git a/test/clj/rems/db/test_form.clj b/test/clj/rems/db/test_form.clj new file mode 100644 index 0000000000..1773cda9c0 --- /dev/null +++ b/test/clj/rems/db/test_form.clj @@ -0,0 +1,42 @@ +(ns ^:integration rems.db.test-form + (:require [clojure.test :refer [deftest is testing use-fixtures]] + [rems.cache :as cache] + [rems.db.form :as form] + [rems.db.test-data-helpers :as test-helpers] + [rems.db.testing :refer [test-db-fixture rollback-db-fixture]])) + +(use-fixtures :once test-db-fixture) +(use-fixtures :each rollback-db-fixture) + +(deftest test-form-template-cache + (testing "cache reload works" + (let [org-id (test-helpers/create-organization! {}) + form-id (test-helpers/create-form! {:organization {:organization/id org-id} + :form/internal-name "test-form" + :form/external-title {:en "Test Form" + :fi "Testilomake" + :sv "Testformulär"} + :form/fields [{:field/type :text + :field/title {:en "Text field" + :fi "Tekstikenttä" + :sv "Textfält"} + :field/optional false}]})] + ;; force cache reload + (cache/set-uninitialized! rems.db.form/form-template-cache) + + (is (= {form-id {:form/id form-id + :organization {:organization/id org-id} + :form/internal-name "test-form" + :form/external-title {:fi "Testilomake" + :en "Test Form" + :sv "Testformulär"} + :form/title "test-form" ; deprecated + :form/fields [{:field/title {:fi "Tekstikenttä" + :en "Text field" + :sv "Textfält"} + :field/type :text + :field/id "fld1" + :field/optional false}] + :enabled true + :archived false}} + (into {} (cache/entries! rems.db.form/form-template-cache))))))) \ No newline at end of file diff --git a/test/clj/rems/db/test_licenses.clj b/test/clj/rems/db/test_licenses.clj new file mode 100644 index 0000000000..ccabf3bf7f --- /dev/null +++ b/test/clj/rems/db/test_licenses.clj @@ -0,0 +1,47 @@ +(ns ^:integration rems.db.test-licenses + (:require [clojure.test :refer [deftest is testing use-fixtures]] + [rems.cache :as cache] + [rems.db.licenses] + [rems.db.test-data-helpers :as test-helpers] + [rems.db.testing :refer [test-db-fixture rollback-db-fixture]])) + +(use-fixtures :once test-db-fixture) +(use-fixtures :each rollback-db-fixture) + +(deftest test-license-cache + (testing "cache reload works" + (let [org-id (test-helpers/create-organization! {}) + license-id (test-helpers/create-license! {:license/type :link + :organization {:organization/id org-id} + :license/title {:en "Test License" + :fi "Testilisenssi" + :sv "Testlicens"} + :license/link {:en "http://example.com/license/en" + :fi "http://example.com/license/fi" + :sv "http://example.com/license/sv"}})] + ;; force cache reload + (cache/set-uninitialized! rems.db.licenses/license-cache) + (cache/set-uninitialized! rems.db.licenses/license-localizations-cache) + + ;; verify license is cached correctly + (is (= {license-id {:id license-id + :licensetype "link" + :organization org-id + :enabled true + :archived false}} + (into {} (cache/entries! rems.db.licenses/license-cache)))) + + (testing "localizations cache" + (is (= {license-id {:en {:licid license-id + :langcode :en + :title "Test License" + :textcontent "http://example.com/license/en"} + :fi {:licid license-id + :langcode :fi + :title "Testilisenssi" + :textcontent "http://example.com/license/fi"} + :sv {:licid license-id + :langcode :sv + :title "Testlicens" + :textcontent "http://example.com/license/sv"}}} + (into {} (cache/entries! rems.db.licenses/license-localizations-cache)))))))) \ No newline at end of file diff --git a/test/clj/rems/db/test_organizations.clj b/test/clj/rems/db/test_organizations.clj new file mode 100644 index 0000000000..51268e2d0e --- /dev/null +++ b/test/clj/rems/db/test_organizations.clj @@ -0,0 +1,35 @@ +(ns ^:integration rems.db.test-organizations + (:require [clojure.test :refer [deftest is testing use-fixtures]] + [rems.cache :as cache] + [rems.db.organizations] + [rems.db.test-data-helpers :as test-helpers] + [rems.db.testing :refer [test-db-fixture rollback-db-fixture]])) + +(use-fixtures :once test-db-fixture) +(use-fixtures :each rollback-db-fixture) + +(deftest test-organization-cache + (testing "cache reload works" + (let [org-id (test-helpers/create-organization! {:organization/id "test-org" + :organization/name {:fi "Testiorganisaatio" + :en "Test Organization" + :sv "Testorganisation"} + :organization/short-name {:fi "Testi" + :en "Test" + :sv "Test"} + :organization/owners [{:userid "owner"}]})] + ;; force cache reload + (cache/set-uninitialized! rems.db.organizations/organization-cache) + + (is (= {org-id {:organization/id org-id + :organization/name {:fi "Testiorganisaatio" + :en "Test Organization" + :sv "Testorganisation"} + :organization/short-name {:fi "Testi" + :en "Test" + :sv "Test"} + :organization/owners [{:userid "owner"}] + :organization/review-emails [] + :enabled true + :archived false}} + (into {} (cache/entries! rems.db.organizations/organization-cache))))))) \ No newline at end of file diff --git a/test/clj/rems/db/test_resource.clj b/test/clj/rems/db/test_resource.clj new file mode 100644 index 0000000000..446c341456 --- /dev/null +++ b/test/clj/rems/db/test_resource.clj @@ -0,0 +1,25 @@ +(ns ^:integration rems.db.test-resource + (:require [clojure.test :refer [deftest is testing use-fixtures]] + [rems.cache :as cache] + [rems.db.resource] + [rems.db.test-data-helpers :as test-helpers] + [rems.db.testing :refer [test-db-fixture rollback-db-fixture]])) + +(use-fixtures :once test-db-fixture) +(use-fixtures :each rollback-db-fixture) + +(deftest test-resource-cache + (testing "cache reload works" + (let [org-id (test-helpers/create-organization! {}) + res-id (test-helpers/create-resource! {:resource-ext-id "test-resource" + :organization {:organization/id org-id} + :resid "https://example.org/resource"})] + ;; force cache reload + (cache/set-uninitialized! rems.db.resource/resource-cache) + + (is (= {res-id {:id res-id + :organization {:organization/id org-id} + :resid "test-resource" + :enabled true + :archived false}} + (into {} (cache/entries! rems.db.resource/resource-cache))))))) \ No newline at end of file diff --git a/test/clj/rems/db/test_roles.clj b/test/clj/rems/db/test_roles.clj index 14462a8848..38c58ccbbd 100644 --- a/test/clj/rems/db/test_roles.clj +++ b/test/clj/rems/db/test_roles.clj @@ -1,5 +1,6 @@ (ns ^:integration rems.db.test-roles (:require [clojure.test :refer [deftest is testing use-fixtures]] + [rems.cache :as cache] [rems.db.roles] [rems.db.testing :refer [rollback-db-fixture test-db-fixture]] [rems.db.users])) @@ -41,6 +42,17 @@ (is (= #{"user"} (rems.db.roles/get-users-with-role :owner))) (is (= #{"user"} (rems.db.roles/get-users-with-role :reporter)))) + (testing "cache reload works" + ;; force cache reload + (cache/set-uninitialized! rems.db.roles/role-cache) + (is (= {"user" #{:owner :reporter}} + (into {} (cache/entries! rems.db.roles/role-cache)))) + + (testing "dependent caches" + (is (= {:owner #{"user"} + :reporter #{"user"}} + (into {} (cache/entries! @#'rems.db.roles/users-by-role)))))) + (testing "remove owner role" (rems.db.roles/remove-role! "user" :owner) (is (= #{:logged-in :reporter} (rems.db.roles/get-roles "user"))) diff --git a/test/clj/rems/db/test_user_mappings.clj b/test/clj/rems/db/test_user_mappings.clj new file mode 100644 index 0000000000..0578a7e1f4 --- /dev/null +++ b/test/clj/rems/db/test_user_mappings.clj @@ -0,0 +1,62 @@ +(ns ^:integration rems.db.test-user-mappings + (:require [clojure.test :refer [deftest is testing use-fixtures]] + [rems.cache :as cache] + [rems.db.testing :refer [rollback-db-fixture test-db-fixture]] + [rems.db.user-mappings] + [rems.db.test-data-helpers :as test-helpers])) + +(use-fixtures :once test-db-fixture) +(use-fixtures :each rollback-db-fixture) + +(deftest test-user-mapping-caches + (testing "cache reload works" + (let [user-1 (test-helpers/create-user! {:userid "alice"}) + user-2 (test-helpers/create-user! {:userid "bob"}) + _ (rems.db.user-mappings/create-user-mapping! {:userid user-1 + :ext-id-attribute "elixirId" + :ext-id-value "elixir-alice"}) + _ (rems.db.user-mappings/create-user-mapping! {:userid user-2 + :ext-id-attribute "elixirId" + :ext-id-value "elixir-bob"}) + _ (rems.db.user-mappings/create-user-mapping! {:userid user-2 + :ext-id-attribute "altId" + :ext-id-value "bob-alt"})] + ;; force cache reload + (cache/set-uninitialized! rems.db.user-mappings/user-mappings-cache) + + ;; verify user-mappings-cache is cached correctly + (is (= {"alice" [{:userid "alice" + :ext-id-attribute "elixirId" + :ext-id-value "elixir-alice"}] + "bob" [{:userid "bob" + :ext-id-attribute "elixirId" + :ext-id-value "elixir-bob"} + {:userid "bob" + :ext-id-attribute "altId" + :ext-id-value "bob-alt"}]} + (into {} (cache/entries! rems.db.user-mappings/user-mappings-cache)))) + + (testing "dependent caches" + ;; verify by-extidattribute is cached correctly (depends on user-mappings-cache) + (is (= {"elixirId" [{:userid "alice" + :ext-id-attribute "elixirId" + :ext-id-value "elixir-alice"} + {:userid "bob" + :ext-id-attribute "elixirId" + :ext-id-value "elixir-bob"}] + "altId" [{:userid "bob" + :ext-id-attribute "altId" + :ext-id-value "bob-alt"}]} + (into {} (cache/entries! @#'rems.db.user-mappings/by-extidattribute)))) + + ;; verify by-extidvalue is cached correctly (depends on user-mappings-cache) + (is (= {"elixir-alice" [{:userid "alice" + :ext-id-attribute "elixirId" + :ext-id-value "elixir-alice"}] + "elixir-bob" [{:userid "bob" + :ext-id-attribute "elixirId" + :ext-id-value "elixir-bob"}] + "bob-alt" [{:userid "bob" + :ext-id-attribute "altId" + :ext-id-value "bob-alt"}]} + (into {} (cache/entries! @#'rems.db.user-mappings/by-extidvalue)))))))) \ No newline at end of file diff --git a/test/clj/rems/db/test_users.clj b/test/clj/rems/db/test_users.clj index 9fbfc610e9..0991413817 100644 --- a/test/clj/rems/db/test_users.clj +++ b/test/clj/rems/db/test_users.clj @@ -30,6 +30,19 @@ :organizations [{:organization/id "org"}]} (rems.db.users/get-user "user-with-org"))) + (testing "cache reload works" + ;; force cache reload + (cache/set-uninitialized! rems.db.users/user-cache) + (is (= {"user-with-org" {:userid "user-with-org" + :name "User Org" + :email "user@org" + :organizations [{:organization/id "org"}]} + "user1" {:userid "user1" + :name "What Ever" + :email nil + :some-attr "some value"}} + (into {} (cache/entries! rems.db.users/user-cache))))) + (testing "user has different userid in userattrs" ;; NB: raw db call due testing backwards compatible behavior (db/add-user! {:user "different-userid" :userattrs (json/generate-string {:userid "bad"})}) diff --git a/test/clj/rems/db/test_workflow.clj b/test/clj/rems/db/test_workflow.clj index 4c8c8e9f85..cd8df2f94c 100644 --- a/test/clj/rems/db/test_workflow.clj +++ b/test/clj/rems/db/test_workflow.clj @@ -1,8 +1,9 @@ (ns ^:integration rems.db.test-workflow - (:require [clojure.test :refer :all] + (:require [clojure.test :refer [deftest is testing use-fixtures]] [rems.db.testing :refer [rollback-db-fixture test-db-fixture]] [rems.db.workflow] - [rems.db.test-data-helpers :as test-helpers])) + [rems.db.test-data-helpers :as test-helpers] + [rems.cache :as cache])) (use-fixtures :once test-db-fixture) (use-fixtures :each rollback-db-fixture) @@ -16,39 +17,65 @@ (is (= #{:handler} (rems.db.workflow/get-all-workflow-roles "handler-user"))))) (deftest test-crud-workflow - (testing "creating" - (let [id (rems.db.workflow/create-workflow! {:userid "owner" - :organization {:organization/id "abc"} - :type :workflow/default - :title "test-crud-workflow-title" - :handlers ["bob" "handler"] - :forms [] - :licenses []})] - (is (number? id)) - (is (= {:archived false - :organization {:organization/id "abc"} - :title "test-crud-workflow-title" - :workflow {:type :workflow/default - :handlers [{:userid "bob"} - {:userid "handler"}] - :forms [] - :licenses []} - :id id - :enabled true} - (rems.db.workflow/get-workflow id))) + (let [expected-id (atom nil)] - (testing "editing" - (rems.db.workflow/edit-workflow! {:id id - :handlers ["bob" "handler" "alice"]}) + (testing "creating" + (let [id (rems.db.workflow/create-workflow! {:userid "owner" + :organization {:organization/id "abc"} + :type :workflow/default + :title "test-crud-workflow-title" + :handlers ["bob" "handler"] + :forms [] + :licenses []}) + _ (reset! expected-id id)] + (is (number? id)) (is (= {:archived false :organization {:organization/id "abc"} :title "test-crud-workflow-title" :workflow {:type :workflow/default :handlers [{:userid "bob"} - {:userid "handler"} - {:userid "alice"}] + {:userid "handler"}] :forms [] :licenses []} :id id :enabled true} - (rems.db.workflow/get-workflow id))))))) + (rems.db.workflow/get-workflow id))))) + + (testing "cache reload works" + (cache/set-uninitialized! rems.db.workflow/workflow-cache) + (is (= {@expected-id {:archived false + :organization {:organization/id "abc"} + :title "test-crud-workflow-title" + :workflow {:type :workflow/default + :handlers [{:userid "bob"} + {:userid "handler"}] + :forms [] + :licenses []} + :id @expected-id + :enabled true}} + (into {} (cache/entries! rems.db.workflow/workflow-cache))) + "cache contains the workflow")) + + (testing "editing" + (rems.db.workflow/edit-workflow! {:id @expected-id + :handlers ["bob" "handler" "alice"]}) + (is (= {:archived false + :organization {:organization/id "abc"} + :title "test-crud-workflow-title" + :workflow {:type :workflow/default + :handlers [{:userid "bob"} + {:userid "handler"} + {:userid "alice"}] + :forms [] + :licenses []} + :id @expected-id + :enabled true} + (rems.db.workflow/get-workflow @expected-id))) + + (testing "archive status" + (rems.db.workflow/set-archived! @expected-id true) + (is (:archived (rems.db.workflow/get-workflow @expected-id)))) + + (testing "enabled status" + (rems.db.workflow/set-enabled! @expected-id false) + (is (not (:enabled (rems.db.workflow/get-workflow @expected-id)))))))) From 671f1e4324a1ba6e639e8b60e49aff0c384dc4e0 Mon Sep 17 00:00:00 2001 From: Anssi Kinnunen Date: Tue, 28 Jan 2025 17:07:58 +0200 Subject: [PATCH 02/80] fix: use test names for CircleCI test splitting Change CircleCI test splitting to use test names instead of filenames (documentation was buried deep on this one). This should improve parallel test distribution, especially for browser tests that are all in one file. --- test/clj/rems/kaocha.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/clj/rems/kaocha.clj b/test/clj/rems/kaocha.clj index 9e9292f618..516aaad82d 100644 --- a/test/clj/rems/kaocha.clj +++ b/test/clj/rems/kaocha.clj @@ -18,7 +18,7 @@ :always (keyword))) (defn split-test-ids [test-plan] - (->> (sh/sh "circleci" "tests" "split" "--split-by=timings" + (->> (sh/sh "circleci" "tests" "split" "--split-by=timings" "--timings-type=testname" :in (str/join "\n" (for [test (test-seq test-plan) :when (is-kaocha-var test)] (:kaocha.testable/id test)))) From dbba1dd76c648774d26643da377d137cae29efcc Mon Sep 17 00:00:00 2001 From: Anssi Kinnunen Date: Tue, 28 Jan 2025 17:09:18 +0200 Subject: [PATCH 03/80] tools: store CI artifacts from kaocha test runs - Renamed kaocha plugins - Write cache statistics to files in target/cache-statistics-plugin instead of stdout, and store as CircleCI artifacts - Write test-ids and split-test-ids to files in target/circleci-parallel-plugin and store as CircleCI artifacts --- .circleci/config.yml | 8 +++++- test/clj/rems/kaocha.clj | 57 ++++++++++++++++++++++++++-------------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b6cc864ac3..16fff49125 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -84,6 +84,8 @@ jobs: - run: lein kaocha --reporter kaocha.report/documentation --plugin rems.kaocha/cache-statistics-plugin integration - store_test_results: path: target/test-results + - store_artifacts: + path: target/cache-statistics-plugin browser-test: executor: db @@ -98,7 +100,7 @@ jobs: at: . - restore_cache: key: *cache-key - - run: lein kaocha --reporter kaocha.report/documentation --plugin rems.kaocha/cache-statistics-plugin --plugin rems.kaocha/circleci-plugin browser + - run: lein kaocha --reporter kaocha.report/documentation --plugin rems.kaocha/cache-statistics-plugin --plugin rems.kaocha/circleci-parallel-plugin browser - store_test_results: path: target/test-results - store_artifacts: @@ -107,6 +109,10 @@ jobs: path: browsertest-downloads - store_artifacts: path: browsertest-accessibility-report + - store_artifacts: + path: target/cache-statistics-plugin + - store_artifacts: + path: target/circleci-parallel-plugin build: executor: clojure diff --git a/test/clj/rems/kaocha.clj b/test/clj/rems/kaocha.clj index 516aaad82d..89ee3897eb 100644 --- a/test/clj/rems/kaocha.clj +++ b/test/clj/rems/kaocha.clj @@ -1,6 +1,8 @@ (ns rems.kaocha "Namespace for various Kaocha test runner helpers." - (:require [clojure.java.shell :as sh] + (:require [better-cond.core :as b] + [clojure.java.io :as io] + [clojure.java.shell :as sh] [clojure.string :as str] [kaocha.plugin :as p] [kaocha.result] @@ -10,6 +12,10 @@ [rems.markdown] [rems.service.caches])) +;; directories for storing plugin output +(def cache-statistics-plugin-dir "target/cache-statistics-plugin") +(def circleci-parallel-plugin-dir "target/circleci-parallel-plugin") + (def is-kaocha-var (comp #{:kaocha.type/var} :kaocha.testable/type)) (defn parse-keyword [s] @@ -18,13 +24,22 @@ :always (keyword))) (defn split-test-ids [test-plan] - (->> (sh/sh "circleci" "tests" "split" "--split-by=timings" "--timings-type=testname" - :in (str/join "\n" (for [test (test-seq test-plan) - :when (is-kaocha-var test)] - (:kaocha.testable/id test)))) - :out - (str/split-lines) - (into #{} (map parse-keyword)))) + (let [test-ids (for [test (test-seq test-plan) + :when (is-kaocha-var test)] + (:kaocha.testable/id test)) + test-ids-batch (->> (sh/sh "circleci" "tests" "split" "--split-by=timings" "--timings-type=testname" + :in (str/join "\n" test-ids)) + :out + (str/split-lines) + (into #{} (map parse-keyword))) + test-ids-report-file (io/file circleci-parallel-plugin-dir "test-ids.txt") + split-test-ids-report-file (io/file circleci-parallel-plugin-dir "split-test-ids.txt")] + + (io/make-parents test-ids-report-file) ; shared parent, need only once + (spit test-ids-report-file (str/join "\n" (sort test-ids))) + (spit split-test-ids-report-file (str/join "\n" (sort test-ids-batch))) + + test-ids-batch)) (defn walk-kaocha-tests [m f] (letfn [(recurse [tests] @@ -43,7 +58,7 @@ ;; Plugin that performs runtime test filtering in CircleCI using test splitting. ;; Skips excluded test ids (not in split test batch) before test run. ;; Heavily inspired by https://andreacrotti.github.io/2020-07-28-parallel-ci-kaocha/ -(defmethod p/-register :rems.kaocha/circleci-plugin [_name plugins] +(defmethod p/-register :rems.kaocha/circleci-parallel-plugin [_name plugins] (conj plugins {:kaocha.hooks/post-load (fn [test-plan] ; skip tests that are not included in set of test ids returned by circle ci @@ -106,15 +121,17 @@ {:kaocha.hooks/post-summary (fn [result] (when-not (kaocha.result/failed? result) ; skip performance summary if tests fail - (when-let [stats (->> (rems.db.testing/get-cache-statistics) - group-cache-stats-by-test - (keep enrich-statistics) - seq)] - (println "") - (println (format "Top %d cache users grouped by cache:" top-n-results)) - (println "") - (run! println (rems.markdown/markdown-table - {:header [:id "%" :get :upsert :evict :reload] - :rows (get-tabular-data stats) - :row-fn (juxt :id :% :get :upsert :evict :reload)})))) + (b/when-let [stats (->> (rems.db.testing/get-cache-statistics) + group-cache-stats-by-test + (keep enrich-statistics) + seq) + report-file (io/file cache-statistics-plugin-dir "cache-report.md")] + (io/make-parents report-file) + (spit report-file + (str/join "\n" + (cons (format "Top %d cache users grouped by cache:\n" top-n-results) + (rems.markdown/markdown-table + {:header [:id "%" :get :upsert :evict :reload] + :rows (get-tabular-data stats) + :row-fn (juxt :id :% :get :upsert :evict :reload)})))))) result)})) From 3f898a202961e44b1e258875d172f687016400d5 Mon Sep 17 00:00:00 2001 From: Anssi Kinnunen Date: Tue, 28 Jan 2025 18:10:35 +0200 Subject: [PATCH 04/80] feat: remove cache hit statistics This was unnecessary part from the beginning, and mainly just interesting while debugging early version of the cache. There is a subtle "bug" regarding config startup: mount is not started before the namespace is loaded. It's not an easy issue to solve, because caches are defined in many different namespaces and initialized using function call. --- src/clj/rems/cache.clj | 58 +++++++++++++++---------------- test/clj/rems/kaocha.clj | 6 ++-- test/clj/rems/test_cache.clj | 66 +++++++++++++++++++++++------------- 3 files changed, 72 insertions(+), 58 deletions(-) diff --git a/src/clj/rems/cache.clj b/src/clj/rems/cache.clj index 890996e3dd..7e8304219a 100644 --- a/src/clj/rems/cache.clj +++ b/src/clj/rems/cache.clj @@ -1,7 +1,6 @@ (ns rems.cache (:require [clojure.core.cache.wrapped :as w] [clojure.tools.logging.readable :as logr] - [medley.core :refer [update-existing]] [rems.common.util :refer [build-index]] [rems.common.dependency :as dep] [rems.config])) @@ -11,8 +10,6 @@ (def ^{:doc "Value that tells cache to skip entry."} absent ::absent) -(def ^:private initial-statistics {:get 0 :reload 0 :upsert 0 :evict 0}) - (defprotocol RefreshableCacheProtocol "Protocol for cache wrapper that can refresh the underlying cache." @@ -40,8 +37,6 @@ (export-statistics! [this] "Retrieves runtime statistics from cache, and resets appropriate counters.") - (increment-get-statistic! [this] - "Increments cache access counter.") (increment-reload-statistic! [this] "Increments cache reload counter.") (increment-upsert-statistic! [this] @@ -73,14 +68,13 @@ CacheStatisticsProtocol (export-statistics! [this] - (let [stats @statistics] - (reset! statistics (select-keys initial-statistics (keys stats))) - stats)) + (let [value @statistics] + (reset! statistics {:reload 0 :upsert 0 :evict 0}) + value)) - (increment-get-statistic! [this] (swap! statistics update-existing :get inc)) - (increment-reload-statistic! [this] (swap! statistics update :reload inc)) - (increment-upsert-statistic! [this] (swap! statistics update :upsert inc)) - (increment-evict-statistic! [this] (swap! statistics update :evict inc)) + (increment-reload-statistic! [this] (some-> statistics (swap! update :reload inc))) + (increment-upsert-statistic! [this] (some-> statistics (swap! update :upsert inc))) + (increment-evict-statistic! [this] (some-> statistics (swap! update :evict inc))) RefreshableCacheProtocol @@ -106,8 +100,6 @@ (ensure-initialized! [this] ;; NB: reading requires no locking - (increment-get-statistic! this) - (if @initialized? @the-cache (reload! this))) @@ -184,23 +176,27 @@ (increment-evict-statistic! this) (logr/debug "<" id :evict k))))) -(defn basic [{:keys [depends-on id miss-fn reload-fn]}] +(defn- ensure-cache-id-unique! [id] (when (contains? @caches id) + ;; NB: even if config is not started, we default to assert (if (:dev rems.config/env) (logr/warnf "overriding cache id %s" id) - (assert false (format "error overriding cache id %s" id)))) - (let [initialized? false - statistics (if (:dev rems.config/env) - initial-statistics - (select-keys initial-statistics [:reload :upsert :evict])) ; :get statistics can become big quickly - the-cache (w/basic-cache-factory {}) - cache (->RefreshableCache id - (atom statistics) - (atom initialized?) - the-cache - miss-fn - (or reload-fn (constantly {})))] - (swap! caches assoc id cache) - (swap! caches-dag dep/depend id depends-on) ; noop when empty depends-on - (add-watch the-cache id reset-dependents-on-change!) - cache)) + (assert false (format "error overriding cache id %s" id))))) + +(defn basic [{:keys [depends-on id miss-fn reload-fn]}] + (or (ensure-cache-id-unique! id) + (let [initialized? false + statistics {} + the-cache (w/basic-cache-factory {}) + cache (->RefreshableCache id + (atom statistics) + (atom initialized?) + the-cache + miss-fn + (or reload-fn (constantly {}))) + _ (export-statistics! cache) ; NB: initializes statistics + ] + (swap! caches assoc id cache) + (swap! caches-dag dep/depend id depends-on) ; noop when empty depends-on + (add-watch the-cache id reset-dependents-on-change!) + cache))) diff --git a/test/clj/rems/kaocha.clj b/test/clj/rems/kaocha.clj index 89ee3897eb..3350df213e 100644 --- a/test/clj/rems/kaocha.clj +++ b/test/clj/rems/kaocha.clj @@ -81,7 +81,7 @@ :when (some? c)] {id {test-id c}})))) -(defn- sum-total [x] (+ (:get x 0) (:reload x 0) (:upsert x 0) (:evict x 0))) +(defn- sum-total [x] (+ (:reload x 0) (:upsert x 0) (:evict x 0))) (defn- enrich-statistics [[cache-id by-test-id]] (let [total-stats (apply merge-with + (vals by-test-id)) @@ -131,7 +131,7 @@ (str/join "\n" (cons (format "Top %d cache users grouped by cache:\n" top-n-results) (rems.markdown/markdown-table - {:header [:id "%" :get :upsert :evict :reload] + {:header [:id "%" :upsert :evict :reload] :rows (get-tabular-data stats) - :row-fn (juxt :id :% :get :upsert :evict :reload)})))))) + :row-fn (juxt :id :% :upsert :evict :reload)})))))) result)})) diff --git a/test/clj/rems/test_cache.clj b/test/clj/rems/test_cache.clj index 4a8cbb9601..4b128ee902 100644 --- a/test/clj/rems/test_cache.clj +++ b/test/clj/rems/test_cache.clj @@ -3,7 +3,6 @@ [clojure.test :refer [deftest is testing]] [clojure.tools.logging.readable :as logr] [clojure.tools.logging.test :as log-test] - [clojure.walk] [medley.core :refer [map-vals]] [rems.cache :as cache] [rems.common.dependency :as dep] @@ -22,12 +21,12 @@ (defn- get-cache-entries "Returns cache as hash map that can be asserted with =" [c] - (clojure.walk/keywordize-keys (cache/entries! c))) + (into {} (cache/entries! c))) (defn- get-cache-raw "Like get-cache-entries, but does not trigger cache readyness mechanisms." [c] - (clojure.walk/keywordize-keys @(get c :the-cache))) + (into {} @(get c :the-cache))) (def ^:private miss-fn (constantly {:value true})) (def ^:private reload-fn (constantly {:always {:value :special}})) @@ -36,9 +35,15 @@ (with-redefs [rems.cache/caches (doto caches (reset! nil)) rems.cache/caches-dag (doto caches-dag (reset! (dep/make-graph))) rems.config/env {:dev true}] - (let [c (cache/basic {:id ::test-cache - :miss-fn (fn [id] (miss-fn id)) - :reload-fn (fn [] (reload-fn))})] + (let [miss-fn-calls (atom []) + reload-fn-call-count (atom 0) + c (cache/basic {:id ::test-cache + :miss-fn (fn [id] + (swap! miss-fn-calls conj id) + (miss-fn id)) + :reload-fn (fn [] + (swap! reload-fn-call-count inc) + (reload-fn))})] (is (= c (get @caches ::test-cache))) (is (= [] @@ -50,38 +55,40 @@ (get-cache-raw c))) (is (= false (deref (:initialized? c)))) - (is (= {:evict 0 :get 0 :reload 0 :upsert 0} + (is (= {:evict 0 :reload 0 :upsert 0} (cache/export-statistics! c)))) (testing "entries reloads cache" (is (= {:always {:value :special}} (get-cache-entries c))) - (is (= {:evict 0 :get 1 :reload 1 :upsert 0} - (cache/export-statistics! c))))) + (is (= {:evict 0 :reload 1 :upsert 0} + (cache/export-statistics! c))) + (is (= 1 + @reload-fn-call-count)))) (testing "lookup" (is (= nil (cache/lookup! c :a))) (is (= {:value :special} (cache/lookup! c :always))) - (is (= {:evict 0 :get 2 :reload 0 :upsert 0} - (cache/export-statistics! c))) - (is (= {:always {:value :special}} - (get-cache-raw c)))) + (is (= {:evict 0 :reload 0 :upsert 0} + (cache/export-statistics! c)))) (testing "lookup-or-miss" (testing "existing entry should not trigger cache miss" (is (= {:value :special} (cache/lookup-or-miss! c :always))) - (is (= {:evict 0 :get 1 :reload 0 :upsert 0} - (cache/export-statistics! c))) - (is (= {:always {:value :special}} - (get-cache-raw c)))) + (is (= [] + @miss-fn-calls)) + (is (= {:evict 0 :reload 0 :upsert 0} + (cache/export-statistics! c)))) (testing "non-existing entry should be added on cache miss" (is (= {:value true} (cache/lookup-or-miss! c :a))) - (is (= {:evict 0 :get 2 :reload 0 :upsert 1} + (is (= [:a] + @miss-fn-calls)) + (is (= {:evict 0 :reload 0 :upsert 1} (cache/export-statistics! c))) (is (= {:a {:value true} :always {:value :special}} @@ -90,8 +97,10 @@ (testing "absent value skips cache entry" (with-redefs [miss-fn (constantly cache/absent)] (is (= nil - (cache/lookup-or-miss! c :test-skip)))) - (is (= {:evict 0 :get 1 :reload 0 :upsert 0} + (cache/lookup-or-miss! c :test-skip))) + (is (= [:a :test-skip] + @miss-fn-calls))) + (is (= {:evict 0 :reload 0 :upsert 0} (cache/export-statistics! c))) (is (= {:a {:value true} :always {:value :special}} @@ -101,29 +110,38 @@ (cache/evict! c :a) (is (= nil (cache/lookup! c :a))) - (is (= {:evict 1 :get 3 :reload 0 :upsert 0} + (is (= {:evict 1 :reload 0 :upsert 0} (cache/export-statistics! c))) (is (= {:always {:value :special}} (get-cache-raw c))) (testing "non-existing entry does nothing" (cache/evict! c :does-not-exist) - (is (= {:evict 0 :get 1 :reload 0 :upsert 0} + (is (= {:evict 0 :reload 0 :upsert 0} (cache/export-statistics! c))) (is (= {:always {:value :special}} (get-cache-raw c))))) (testing "miss" (cache/miss! c :new-entry) + (is (= [:a :test-skip :new-entry] + @miss-fn-calls)) (is (= {:value true} (cache/lookup! c :new-entry))) + (is (= {:value true} (cache/lookup-or-miss! c :new-entry))) - (is (= {:evict 0 :get 4 :reload 0 :upsert 1} + (is (= [:a :test-skip :new-entry] + @miss-fn-calls)) + + (is (= {:evict 0 :reload 0 :upsert 1} (cache/export-statistics! c))) (is (= {:always {:value :special} :new-entry {:value true}} - (get-cache-raw c))))))) + (get-cache-raw c)))) + + (is (= 1 @reload-fn-call-count) + "cache has been reloaded exactly once")))) (deftest test-cache-dependencies (testing "cannot create caches with circular dependencies" From 5cf0c5229edaf5f09f808136ca86dfca6683fc39 Mon Sep 17 00:00:00 2001 From: Anssi Kinnunen Date: Wed, 29 Jan 2025 09:45:51 +0200 Subject: [PATCH 05/80] chore: re-require rems.main in browser tests rems.main was removed at some point as unnecessary require, but it is now used by hooks test, and sometimes Kaocha test runner goes haywire about the missing require. --- test/clj/rems/test_browser.clj | 1 + 1 file changed, 1 insertion(+) diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj index 24a14df162..ca7c88cf70 100644 --- a/test/clj/rems/test_browser.clj +++ b/test/clj/rems/test_browser.clj @@ -27,6 +27,7 @@ [rems.db.test-data-users :as test-users] [rems.db.user-settings] [rems.handler] + [rems.main] [rems.service.application] [rems.service.catalogue] [rems.service.form] From c107eeedf1e64833462a88451b87c1a5004d1613 Mon Sep 17 00:00:00 2001 From: Markku Rontu Date: Wed, 2 Jul 2025 22:11:07 +0300 Subject: [PATCH 06/80] fix: properly format audit log response We should use `ok` helper like elsewhere. --- src/clj/rems/api/audit_log.clj | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/clj/rems/api/audit_log.clj b/src/clj/rems/api/audit_log.clj index 030ea20db1..268a96b023 100644 --- a/src/clj/rems/api/audit_log.clj +++ b/src/clj/rems/api/audit_log.clj @@ -1,6 +1,7 @@ (ns rems.api.audit-log (:require [compojure.api.sweet :refer :all] [rems.db.core :as db] + [ring.util.http-response :refer [ok]] [schema.core :as s]) (:import (org.joda.time DateTime))) @@ -23,8 +24,8 @@ {before :- (describe DateTime "Only show entries before this time") nil}] :roles #{:reporter} :return [AuditLogEntry] - (db/get-audit-log {:userid userid - :after after - :path (when application-id - (str "/api/applications/" application-id "%")) - :before before})))) + (ok (db/get-audit-log {:userid userid + :after after + :path (when application-id + (str "/api/applications/" application-id "%")) + :before before}))))) From a2ade30d52a934dd46d3c84a2522927c3417413b Mon Sep 17 00:00:00 2001 From: Markku Rontu Date: Wed, 2 Jul 2025 22:21:07 +0300 Subject: [PATCH 07/80] doc: update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c165a64fc..5529c14a1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Changes since v2.38.1 ### Fixes - Resolved issue where workflow voting could not be removed, which caused UI to display raw translation keys due to nil voting values. (#3357) +- Fix audit log API. Technical error from incorrect response. (#3380) ## v2.38.1 "Välimerenkatu +1" 2025-01-27 From 96ead79f05e97220671873177bd5fa351190a49c Mon Sep 17 00:00:00 2001 From: Meeri Hakala <122456343+meericsc@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:56:34 +0300 Subject: [PATCH 08/80] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 1d9fdea952..5cb3f867dc 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,7 @@ You can check out the API docs using [the swagger-ui of the public demo instance](https://rems-test.2.rahtiapp.fi/swagger-ui). Documentation can be found under the [docs](./docs) folder. + +## See Also + +[REMS-COmanage bridge service](https://github.com/GUARDIANS-infrastructure/rems-co) – Service that synchronizes entitlements between REMS and a COManage registry. From 26dbf97e8ae57177a05f5f68def73f5aab4aeeb6 Mon Sep 17 00:00:00 2001 From: Markku Rontu Date: Wed, 2 Jul 2025 22:11:07 +0300 Subject: [PATCH 09/80] fix: properly format audit log response We should use `ok` helper like elsewhere. --- src/clj/rems/api/audit_log.clj | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/clj/rems/api/audit_log.clj b/src/clj/rems/api/audit_log.clj index 030ea20db1..268a96b023 100644 --- a/src/clj/rems/api/audit_log.clj +++ b/src/clj/rems/api/audit_log.clj @@ -1,6 +1,7 @@ (ns rems.api.audit-log (:require [compojure.api.sweet :refer :all] [rems.db.core :as db] + [ring.util.http-response :refer [ok]] [schema.core :as s]) (:import (org.joda.time DateTime))) @@ -23,8 +24,8 @@ {before :- (describe DateTime "Only show entries before this time") nil}] :roles #{:reporter} :return [AuditLogEntry] - (db/get-audit-log {:userid userid - :after after - :path (when application-id - (str "/api/applications/" application-id "%")) - :before before})))) + (ok (db/get-audit-log {:userid userid + :after after + :path (when application-id + (str "/api/applications/" application-id "%")) + :before before}))))) From c1da00b4b8e60d8f060c9014259b2ef107587409 Mon Sep 17 00:00:00 2001 From: Markku Rontu Date: Wed, 2 Jul 2025 22:21:07 +0300 Subject: [PATCH 10/80] doc: update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c165a64fc..5529c14a1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Changes since v2.38.1 ### Fixes - Resolved issue where workflow voting could not be removed, which caused UI to display raw translation keys due to nil voting values. (#3357) +- Fix audit log API. Technical error from incorrect response. (#3380) ## v2.38.1 "Välimerenkatu +1" 2025-01-27 From bd48825a041ffb3af9f2687ba4bc833ab65111b7 Mon Sep 17 00:00:00 2001 From: Markku Rontu Date: Mon, 27 Oct 2025 11:51:52 +0200 Subject: [PATCH 11/80] fix: temporary fix to cprop to enable JVM 25 support This can be removed once cprop fix is released. See https://github.com/tolitius/cprop/issues/60 --- CHANGELOG.md | 1 + src/clj/rems/config.clj | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5529c14a1f..a47aca91e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Changes since v2.38.1 ### Fixes - Resolved issue where workflow voting could not be removed, which caused UI to display raw translation keys due to nil voting values. (#3357) - Fix audit log API. Technical error from incorrect response. (#3380) +- Temporary fix to enable JVM 25 support (https://github.com/tolitius/cprop/issues/60) ## v2.38.1 "Välimerenkatu +1" 2025-01-27 diff --git a/src/clj/rems/config.clj b/src/clj/rems/config.clj index 4d6e13585e..5e6f866655 100644 --- a/src/clj/rems/config.clj +++ b/src/clj/rems/config.clj @@ -3,7 +3,7 @@ [clojure.java.io :as io] [clojure.test :refer :all] [clojure.tools.logging :as log] - [cprop.core :refer [load-config]] + [cprop.core] [cprop.source :as source] [cprop.tools :refer [merge-maps]] [medley.core :refer [update-existing]] @@ -14,6 +14,14 @@ (:import [java.io FileNotFoundException] [org.joda.time Period])) +(defn load-config [& args] + ;; XXX: workaround for JVM 25 for cprop until fix is released https://github.com/tolitius/cprop/issues/60 + (let [expand-home cprop.tools/expand-home] + (with-redefs [cprop.tools/expand-home #(if (empty? %) + nil + (expand-home %))] + (apply cprop.core/load-config args)))) + (defn- file-sibling [file sibling-name] (.getPath (io/file (.getParentFile (io/file file)) sibling-name))) From e7ba9678305badc5e3b99d138aee1eecdc38919c Mon Sep 17 00:00:00 2001 From: Markku Rontu Date: Tue, 28 Oct 2025 14:25:50 +0200 Subject: [PATCH 12/80] fix: force casting to long because reflection no longer works In these places since JDK 19+ reflection does not find the method without explicitly using long. --- test/clj/rems/db/test_transactions.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/clj/rems/db/test_transactions.clj b/test/clj/rems/db/test_transactions.clj index 9d9eee1754..8bdf01e220 100644 --- a/test/clj/rems/db/test_transactions.clj +++ b/test/clj/rems/db/test_transactions.clj @@ -137,7 +137,7 @@ (fn [] (cache-invalidater) ;; give some mercy - (Thread/sleep (rand-int 50))))))) + (Thread/sleep (long (rand-int 50)))))))) writes-count (atom 0) progress-count (atom -1) ; start below writes-count for the first round writers (submit-all thread-pool (for [app-id app-ids @@ -155,7 +155,7 @@ ;; if there is no progress within 100ms ;; something is wrong, like deadlock (reset! progress-count @writes-count) - (Thread/sleep 100)) + (Thread/sleep (long 100))) (log/info "Finished with " @writes-count "of" target-writes "writes") From c875d449104f98c66b15dea0ae74b02e40ccaa59 Mon Sep 17 00:00:00 2001 From: Markku Rontu Date: Tue, 28 Oct 2025 14:28:32 +0200 Subject: [PATCH 13/80] doc: update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a47aca91e8..a520a92d3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ Changes since v2.38.1 ### Fixes - Resolved issue where workflow voting could not be removed, which caused UI to display raw translation keys due to nil voting values. (#3357) - Fix audit log API. Technical error from incorrect response. (#3380) -- Temporary fix to enable JVM 25 support (https://github.com/tolitius/cprop/issues/60) +- Fixes to enable JVM 25 support (https://github.com/tolitius/cprop/issues/60, https://clojure.atlassian.net/browse/CLJ-2764) ## v2.38.1 "Välimerenkatu +1" 2025-01-27 From 2128861cc9a30ba70455f8a53a36c34fb0bfaf8e Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Thu, 30 Oct 2025 16:48:19 +0200 Subject: [PATCH 14/80] fix: links --- README.md | 2 +- docs/presentations/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5cb3f867dc..3ce878f016 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ See the [release page](https://github.com/CSCfi/rems/releases) for the releases. - We welcome [issues through GitHub](https://github.com/CSCfi/rems/issues). - You can [contact the team on the discussion board](https://github.com/CSCfi/rems/discussions) or [via email](mailto:rems@csc.fi). - In case you would like to contribute to the development, perhaps you participate in Hacktoberfest, then please refer to the [contributing document](CONTRIBUTING.md). We'll guide you through to get your pull request reviewed and merged. -- You can follow the progress of the project on the [GitHub project board]([https://github.com/CSCfi/rems/projects/1](https://github.com/orgs/CSCfi/projects/13)). +- You can follow the progress of the project on the [GitHub project board](https://github.com/orgs/CSCfi/projects/13/views/1). ## Documentation diff --git a/docs/presentations/README.md b/docs/presentations/README.md index f4a597f64a..e7d6db6493 100644 --- a/docs/presentations/README.md +++ b/docs/presentations/README.md @@ -4,11 +4,11 @@ Here we have presentation materials for REMS. Each presentation is in the form o ## How to use – reveal.js -Follow the [https://revealjs.com/installation/#full-setup](instructions) to setup reveal.js, and off you go. +Follow the [instructions](https://revealjs.com/installation/#full-setup) to setup reveal.js, and off you go. ## How to use – CSC's training template -This is a bit more cumbersome than the previous example but does not require setting up a local web server. [https://pandoc.org](Pandoc) is required, however. +This is a bit more cumbersome than the previous example but does not require setting up a local web server. [Pandoc](https://pandoc.org) is required, however. 1. `git clone https://github.com/csc-training/slide-template` From d310b9ff682bc6ccadf14f49a2331e83ebdb58fd Mon Sep 17 00:00:00 2001 From: mvuorio Date: Fri, 31 Oct 2025 14:52:52 +0200 Subject: [PATCH 15/80] filter out existing members from the 'Add member' dropdown menu --- src/cljs/rems/actions/add_member.cljs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/cljs/rems/actions/add_member.cljs b/src/cljs/rems/actions/add_member.cljs index 2df77b5668..b99bc4faf6 100644 --- a/src/cljs/rems/actions/add_member.cljs +++ b/src/cljs/rems/actions/add_member.cljs @@ -26,9 +26,18 @@ (rf/reg-event-db ::set-potential-members (fn [db [_ members]] - (assoc db - ::potential-members (set (map atoms/enrich-user members)) - ::selected-member nil))) + (let [application (-> db :rems.application/application :data) + applicant (-> application :application/applicant) + existing-ids (set (concat + [(:userid applicant)] + (map :userid (:application/members application)))) + filtered (->> members + (filter #(not (contains? existing-ids (:userid %)))) + (map atoms/enrich-user))] + (prn (:application/members application)) + (assoc db + ::potential-members filtered + ::selected-member nil)))) (rf/reg-event-db ::set-selected-member (fn [db [_ member]] (assoc db ::selected-member member))) (rf/reg-sub ::selected-member (fn [db _] (::selected-member db))) From 8f5db3b0d4eaa81a247c86d90ee277663419fb71 Mon Sep 17 00:00:00 2001 From: mvuorio Date: Fri, 31 Oct 2025 15:18:20 +0200 Subject: [PATCH 16/80] remove print statement --- src/cljs/rems/actions/add_member.cljs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cljs/rems/actions/add_member.cljs b/src/cljs/rems/actions/add_member.cljs index b99bc4faf6..2cb5fec5f6 100644 --- a/src/cljs/rems/actions/add_member.cljs +++ b/src/cljs/rems/actions/add_member.cljs @@ -34,7 +34,6 @@ filtered (->> members (filter #(not (contains? existing-ids (:userid %)))) (map atoms/enrich-user))] - (prn (:application/members application)) (assoc db ::potential-members filtered ::selected-member nil)))) From 83d64c01ce8f618077da06b83e6a4a45f5d5a977 Mon Sep 17 00:00:00 2001 From: mvuorio Date: Fri, 31 Oct 2025 15:23:26 +0200 Subject: [PATCH 17/80] filter out existing members from the 'Add member' dropdown menu --- src/cljs/rems/actions/add_member.cljs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cljs/rems/actions/add_member.cljs b/src/cljs/rems/actions/add_member.cljs index 2cb5fec5f6..61f2513446 100644 --- a/src/cljs/rems/actions/add_member.cljs +++ b/src/cljs/rems/actions/add_member.cljs @@ -28,14 +28,14 @@ (fn [db [_ members]] (let [application (-> db :rems.application/application :data) applicant (-> application :application/applicant) - existing-ids (set (concat - [(:userid applicant)] + existing-ids (set (concat + [(:userid applicant)] (map :userid (:application/members application)))) - filtered (->> members + filtered (->> (set members) (filter #(not (contains? existing-ids (:userid %)))) (map atoms/enrich-user))] (assoc db - ::potential-members filtered + ::potential-members (set filtered) ::selected-member nil)))) (rf/reg-event-db ::set-selected-member (fn [db [_ member]] (assoc db ::selected-member member))) From cc019fc5c1878fd9b5c0708725bdc3dc52655dac Mon Sep 17 00:00:00 2001 From: mvuorio Date: Fri, 31 Oct 2025 15:40:06 +0200 Subject: [PATCH 18/80] filter out existing members from the 'Add member' dropdown menu --- src/cljs/rems/actions/add_member.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cljs/rems/actions/add_member.cljs b/src/cljs/rems/actions/add_member.cljs index 61f2513446..57ed91bcca 100644 --- a/src/cljs/rems/actions/add_member.cljs +++ b/src/cljs/rems/actions/add_member.cljs @@ -35,7 +35,7 @@ (filter #(not (contains? existing-ids (:userid %)))) (map atoms/enrich-user))] (assoc db - ::potential-members (set filtered) + ::potential-members filtered ::selected-member nil)))) (rf/reg-event-db ::set-selected-member (fn [db [_ member]] (assoc db ::selected-member member))) From c3bc01162a8c10085b5c1842c8756650d0772a01 Mon Sep 17 00:00:00 2001 From: mvuorio Date: Tue, 4 Nov 2025 22:18:44 +0200 Subject: [PATCH 19/80] filter out existing members from the 'Add member' dropdown menu --- src/cljs/rems/actions/add_member.cljs | 30 ++++++++++++++------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/cljs/rems/actions/add_member.cljs b/src/cljs/rems/actions/add_member.cljs index 57ed91bcca..2d5854218b 100644 --- a/src/cljs/rems/actions/add_member.cljs +++ b/src/cljs/rems/actions/add_member.cljs @@ -15,28 +15,30 @@ :error-handler (flash-message/default-error-handler :top "Fetch potential members")}))) (rf/reg-event-fx - ::open-form + ::reset-form (fn [{:keys [db]} _] {:db (assoc db ::potential-members #{} ::selected-member nil) ::fetch-potential-members #(rf/dispatch [::set-potential-members %])})) -(rf/reg-sub ::potential-members (fn [db _] (::potential-members db))) +(defn potential-members [db] + (let [application (get-in db [:rems.application/application :data]) + applicant (application :application/applicant) + existing-ids (set (concat [(:userid applicant)] + (map :userid (:application/members application))))] + (->> ::potential-members db + (remove (comp existing-ids :userid)) + (map atoms/enrich-user)))) + +(rf/reg-sub ::potential-members (fn [db _] (potential-members db))) + (rf/reg-event-db ::set-potential-members (fn [db [_ members]] - (let [application (-> db :rems.application/application :data) - applicant (-> application :application/applicant) - existing-ids (set (concat - [(:userid applicant)] - (map :userid (:application/members application)))) - filtered (->> (set members) - (filter #(not (contains? existing-ids (:userid %)))) - (map atoms/enrich-user))] - (assoc db - ::potential-members filtered - ::selected-member nil)))) + (assoc db + ::potential-members members + ::selected-member nil))) (rf/reg-event-db ::set-selected-member (fn [db [_ member]] (assoc db ::selected-member member))) (rf/reg-sub ::selected-member (fn [db _] (::selected-member db))) @@ -63,7 +65,7 @@ (defn add-member-action-button [] [action-button {:id action-form-id :text (text :t.actions/add-member) - :on-click #(rf/dispatch [::open-form])}]) + :on-click #(rf/dispatch [::reset-form])}]) (defn add-member-view [{:keys [selected-member potential-members on-set-member on-send]}] From 3c54890d7c0cccc78564c7b01651a9b9eeab9b7a Mon Sep 17 00:00:00 2001 From: mvuorio Date: Tue, 4 Nov 2025 22:36:44 +0200 Subject: [PATCH 20/80] filter out existing members from the 'Add member' dropdown menu --- src/cljs/rems/actions/add_member.cljs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cljs/rems/actions/add_member.cljs b/src/cljs/rems/actions/add_member.cljs index 2d5854218b..de070a5301 100644 --- a/src/cljs/rems/actions/add_member.cljs +++ b/src/cljs/rems/actions/add_member.cljs @@ -15,7 +15,7 @@ :error-handler (flash-message/default-error-handler :top "Fetch potential members")}))) (rf/reg-event-fx - ::reset-form + ::open-form (fn [{:keys [db]} _] {:db (assoc db ::potential-members #{} @@ -65,7 +65,7 @@ (defn add-member-action-button [] [action-button {:id action-form-id :text (text :t.actions/add-member) - :on-click #(rf/dispatch [::reset-form])}]) + :on-click #(rf/dispatch [::open-form])}]) (defn add-member-view [{:keys [selected-member potential-members on-set-member on-send]}] From eb59024f33cc7dd402fd3dfc1c4f1a508993319c Mon Sep 17 00:00:00 2001 From: mvuorio Date: Tue, 4 Nov 2025 22:39:20 +0200 Subject: [PATCH 21/80] filter out existing members from the 'Add member' dropdown menu --- src/cljs/rems/actions/add_member.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cljs/rems/actions/add_member.cljs b/src/cljs/rems/actions/add_member.cljs index de070a5301..685205bd41 100644 --- a/src/cljs/rems/actions/add_member.cljs +++ b/src/cljs/rems/actions/add_member.cljs @@ -36,7 +36,7 @@ (rf/reg-event-db ::set-potential-members (fn [db [_ members]] - (assoc db + (assoc db ::potential-members members ::selected-member nil))) From 6b8974f118ad27de6f44af1847bcd4e6916ab495 Mon Sep 17 00:00:00 2001 From: mvuorio Date: Wed, 5 Nov 2025 06:06:35 +0200 Subject: [PATCH 22/80] filter out existing members from the 'Add member' dropdown menu --- src/cljs/rems/actions/add_member.cljs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cljs/rems/actions/add_member.cljs b/src/cljs/rems/actions/add_member.cljs index 685205bd41..7333c12e62 100644 --- a/src/cljs/rems/actions/add_member.cljs +++ b/src/cljs/rems/actions/add_member.cljs @@ -24,9 +24,10 @@ (defn potential-members [db] (let [application (get-in db [:rems.application/application :data]) - applicant (application :application/applicant) + applicant (get-in application [:application/applicant]) + members (get-in application [:application/members]) existing-ids (set (concat [(:userid applicant)] - (map :userid (:application/members application))))] + (map :userid members)))] (->> ::potential-members db (remove (comp existing-ids :userid)) (map atoms/enrich-user)))) From 9f970de1e639f64f08066827d537b35f8786d7e4 Mon Sep 17 00:00:00 2001 From: mvuorio Date: Wed, 5 Nov 2025 13:06:59 +0200 Subject: [PATCH 23/80] filter out existing members from the 'Add member' dropdown menu --- src/cljs/rems/actions/add_member.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cljs/rems/actions/add_member.cljs b/src/cljs/rems/actions/add_member.cljs index 7333c12e62..6d009cf0a0 100644 --- a/src/cljs/rems/actions/add_member.cljs +++ b/src/cljs/rems/actions/add_member.cljs @@ -32,7 +32,7 @@ (remove (comp existing-ids :userid)) (map atoms/enrich-user)))) -(rf/reg-sub ::potential-members (fn [db _] (potential-members db))) +(rf/reg-sub ::potential-members potential-members) (rf/reg-event-db ::set-potential-members From c092d7241c29b953f083aeab548af070080665d1 Mon Sep 17 00:00:00 2001 From: mvuorio Date: Wed, 5 Nov 2025 13:27:30 +0200 Subject: [PATCH 24/80] CHANGELOG.md: filter out existing members from the 'Add member' dropdown menu --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c165a64fc..245c8ce2c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Changes since v2.38.1 ### Fixes - Resolved issue where workflow voting could not be removed, which caused UI to display raw translation keys due to nil voting values. (#3357) +- Fixed an issue with the application page where the 'Add member' dropdown menu displayed members that already exist in the application. (#1412) ## v2.38.1 "Välimerenkatu +1" 2025-01-27 From f2efc4c7f781aa22993b86a80b4bcc44a5aece3e Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Wed, 5 Nov 2025 15:33:14 +0200 Subject: [PATCH 25/80] fix: hide edit buttons when organization owner is not allowed to modify the resource --- src/cljc/rems/common/roles.cljc | 5 +++++ .../rems/administration/catalogue_item.cljs | 2 +- .../rems/administration/create_workflow.cljs | 3 ++- src/cljs/rems/administration/form.cljs | 3 ++- src/cljs/rems/administration/license.cljs | 2 +- .../rems/administration/organizations.cljs | 21 ++++++++----------- src/cljs/rems/administration/resource.cljs | 2 +- src/cljs/rems/administration/workflow.cljs | 3 +-- 8 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/cljc/rems/common/roles.cljc b/src/cljc/rems/common/roles.cljc index 5ff50518fb..79e6ea0fc3 100644 --- a/src/cljc/rems/common/roles.cljc +++ b/src/cljc/rems/common/roles.cljc @@ -50,3 +50,8 @@ (some (comp #{(-> item :organization :organization/id)} :organization/id)))] (or owner? org-owner?)))) + +#?(:cljs + (defn show-when-can-modify-organization-item [item & body] + (when (can-modify-organization-item? item) + (into [:<> body])))) diff --git a/src/cljs/rems/administration/catalogue_item.cljs b/src/cljs/rems/administration/catalogue_item.cljs index b0c4005b8e..e17ba4b812 100644 --- a/src/cljs/rems/administration/catalogue_item.cljs +++ b/src/cljs/rems/administration/catalogue_item.cljs @@ -104,7 +104,7 @@ (let [id (:id catalogue-item)] [:div.col.commands [administration/back-button "/administration/catalogue-items"] - [roles/show-when roles/+admin-write-roles+ + [roles/show-when-can-modify-organization-item catalogue-item [edit-button id] [perform-action-button (toggle-enabled catalogue-item)] [perform-action-button (toggle-archived catalogue-item)]] diff --git a/src/cljs/rems/administration/create_workflow.cljs b/src/cljs/rems/administration/create_workflow.cljs index d9dc80f263..5f13ac6aaa 100644 --- a/src/cljs/rems/administration/create_workflow.cljs +++ b/src/cljs/rems/administration/create_workflow.cljs @@ -19,7 +19,8 @@ [rems.focus :as focus] [rems.spinner :as spinner] [rems.text :refer [get-localized-title localized localize-command localize-role localize-state text text-format text-format-map]] - [rems.util :refer [navigate! post! put! trim-when-string]])) + [rems.util :refer [navigate! post! put! trim-when-string]] + [rems.common.roles :as roles])) (rf/reg-event-fx ::enter-page (fn [{:keys [db]} [_ workflow-id]] diff --git a/src/cljs/rems/administration/form.cljs b/src/cljs/rems/administration/form.cljs index 90394b31b8..bfd8bd3e0b 100644 --- a/src/cljs/rems/administration/form.cljs +++ b/src/cljs/rems/administration/form.cljs @@ -91,8 +91,9 @@ [:div.col.commands [administration/back-button "/administration/forms"] [roles/show-when roles/+admin-write-roles+ + [copy-as-new-button id]] + [roles/show-when-can-modify-organization-item form [edit-button id] - [copy-as-new-button id] [perform-action-button (toggle-enabled form)] [perform-action-button (toggle-archived form)]]]) (when-let [errors (:form/errors form)] diff --git a/src/cljs/rems/administration/license.cljs b/src/cljs/rems/administration/license.cljs index 8bb4dc65b0..a996598971 100644 --- a/src/cljs/rems/administration/license.cljs +++ b/src/cljs/rems/administration/license.cljs @@ -87,7 +87,7 @@ [:div.col.commands [administration/back-button "/administration/licenses"] - [roles/show-when roles/+admin-write-roles+ + [roles/show-when-can-modify-organization-item license [perform-action-button (toggle-enabled license)] [perform-action-button (toggle-archived license)]]]]) diff --git a/src/cljs/rems/administration/organizations.cljs b/src/cljs/rems/administration/organizations.cljs index 01c5bdbc02..97f6de3cf6 100644 --- a/src/cljs/rems/administration/organizations.cljs +++ b/src/cljs/rems/administration/organizations.cljs @@ -76,21 +76,18 @@ (str "/administration/organizations/" organization-id) (text :t.administration/view)]) -(defn modify-organization-dropdown [organization] - (let [id (:organization/id organization) - org-owner? (->> @(rf/subscribe [:owned-organizations]) - (some (comp #{id} :organization/id)))] - [atoms/commands-group-button - {:label (text :t.actions/modify)} +(defn modify-organization-dropdown [{id :organization/id :as organization}] + [atoms/commands-group-button + {:label (text :t.actions/modify)} - (when org-owner? - (edit-action id)) + (when (roles/can-modify-organization-item? {:organization organization}) + (edit-action id)) ;; XXX: organization owner cannot use these actions currently - (when (roles/has-roles? :owner) - (list - (status-flags/enabled-toggle-action {:on-change #(rf/dispatch [::set-organization-enabled %1 %2 [::fetch-organizations]])} organization) - (status-flags/archived-toggle-action {:on-change #(rf/dispatch [::set-organization-archived %1 %2 [::fetch-organizations]])} organization)))])) + (when (roles/has-roles? :owner) + (list + (status-flags/enabled-toggle-action {:on-change #(rf/dispatch [::set-organization-enabled %1 %2 [::fetch-organizations]])} organization) + (status-flags/archived-toggle-action {:on-change #(rf/dispatch [::set-organization-archived %1 %2 [::fetch-organizations]])} organization)))]) (rf/reg-sub ::organizations-table-rows diff --git a/src/cljs/rems/administration/resource.cljs b/src/cljs/rems/administration/resource.cljs index 1f66442fa6..7fca2fd848 100644 --- a/src/cljs/rems/administration/resource.cljs +++ b/src/cljs/rems/administration/resource.cljs @@ -89,7 +89,7 @@ [resource-blacklist] [:div.col.commands [administration/back-button "/administration/resources"] - [roles/show-when roles/+admin-write-roles+ + [roles/show-when-can-modify-organization-item resource [perform-action-button (toggle-enabled resource)] [perform-action-button (toggle-archived resource)]]]])) diff --git a/src/cljs/rems/administration/workflow.cljs b/src/cljs/rems/administration/workflow.cljs index e650f51305..bcb6f1740a 100644 --- a/src/cljs/rems/administration/workflow.cljs +++ b/src/cljs/rems/administration/workflow.cljs @@ -230,10 +230,9 @@ [voting-fields] [disable-commands-fields] [processing-states-fields] - [:div.col.commands [administration/back-button "/administration/workflows"] - [roles/show-when roles/+admin-write-roles+ + [roles/show-when-can-modify-organization-item workflow [atoms/action-button (edit-workflow-action (:id workflow))] [perform-action-button (toggle-enabled workflow)] [perform-action-button (toggle-archived workflow)]]]])]]) From 3cc4a53168a4f8aa5b3e19d44d3bd07cae78a333 Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Wed, 5 Nov 2025 16:07:43 +0200 Subject: [PATCH 26/80] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c165a64fc..1ae83809a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Changes since v2.38.1 - When creating attachment license, the save button no longer activates until all attachments have been successfully uploaded. (#3292) - Attachment upload error is indicated by small icon next to the upload button (detailed error is still shown in top of the page). - Pending attachment upload is indicated by spinner next to the upload button. +- Edit buttons on the administration pages are hidden when the user is not permitted to edit ### Fixes - Resolved issue where workflow voting could not be removed, which caused UI to display raw translation keys due to nil voting values. (#3357) From e146a0c1d6fc07e17c743094dbd92f0fba298dbe Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Wed, 5 Nov 2025 17:05:13 +0200 Subject: [PATCH 27/80] Remove unused require --- src/cljs/rems/administration/create_workflow.cljs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/cljs/rems/administration/create_workflow.cljs b/src/cljs/rems/administration/create_workflow.cljs index 5f13ac6aaa..d9dc80f263 100644 --- a/src/cljs/rems/administration/create_workflow.cljs +++ b/src/cljs/rems/administration/create_workflow.cljs @@ -19,8 +19,7 @@ [rems.focus :as focus] [rems.spinner :as spinner] [rems.text :refer [get-localized-title localized localize-command localize-role localize-state text text-format text-format-map]] - [rems.util :refer [navigate! post! put! trim-when-string]] - [rems.common.roles :as roles])) + [rems.util :refer [navigate! post! put! trim-when-string]])) (rf/reg-event-fx ::enter-page (fn [{:keys [db]} [_ workflow-id]] From 0cfa274401830d1a6e749036517fcff330b492a8 Mon Sep 17 00:00:00 2001 From: mvuorio Date: Thu, 6 Nov 2025 10:18:34 +0200 Subject: [PATCH 28/80] filter out existing members from the 'Add member' dropdown menu --- src/cljs/rems/actions/add_member.cljs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/cljs/rems/actions/add_member.cljs b/src/cljs/rems/actions/add_member.cljs index 6d009cf0a0..8f9b9d2540 100644 --- a/src/cljs/rems/actions/add_member.cljs +++ b/src/cljs/rems/actions/add_member.cljs @@ -23,14 +23,13 @@ ::fetch-potential-members #(rf/dispatch [::set-potential-members %])})) (defn potential-members [db] - (let [application (get-in db [:rems.application/application :data]) - applicant (get-in application [:application/applicant]) - members (get-in application [:application/members]) - existing-ids (set (concat [(:userid applicant)] - (map :userid members)))] - (->> ::potential-members db - (remove (comp existing-ids :userid)) - (map atoms/enrich-user)))) + (let [application (get-in db [:rems.application/application :data]) + applicant-id (get-in application [:application/applicant :userid]) + members (get-in application [:application/members]) + existing-ids (into #{applicant-id} (map :userid members))] + (->> ::potential-members db + (remove (comp existing-ids :userid)) + (map atoms/enrich-user)))) (rf/reg-sub ::potential-members potential-members) From 3829ccaa04a033566b832224dbf394abd15d484d Mon Sep 17 00:00:00 2001 From: vuorioma Date: Thu, 6 Nov 2025 10:26:44 +0200 Subject: [PATCH 29/80] filter out existing members from the 'Add member' dropdown menu --- src/cljs/rems/actions/add_member.cljs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cljs/rems/actions/add_member.cljs b/src/cljs/rems/actions/add_member.cljs index 8f9b9d2540..354906c65d 100644 --- a/src/cljs/rems/actions/add_member.cljs +++ b/src/cljs/rems/actions/add_member.cljs @@ -26,7 +26,8 @@ (let [application (get-in db [:rems.application/application :data]) applicant-id (get-in application [:application/applicant :userid]) members (get-in application [:application/members]) - existing-ids (into #{applicant-id} (map :userid members))] + existing-ids (into #{applicant-id} + (map :userid members))] (->> ::potential-members db (remove (comp existing-ids :userid)) (map atoms/enrich-user)))) From 0da46bd41578981940498a4a7fb6e2d83c3427e8 Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Thu, 6 Nov 2025 12:55:47 +0200 Subject: [PATCH 30/80] Improve wording --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ae83809a1..d23db4b619 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ Changes since v2.38.1 - When creating attachment license, the save button no longer activates until all attachments have been successfully uploaded. (#3292) - Attachment upload error is indicated by small icon next to the upload button (detailed error is still shown in top of the page). - Pending attachment upload is indicated by spinner next to the upload button. -- Edit buttons on the administration pages are hidden when the user is not permitted to edit +- Edit buttons on the administration detail pages are hidden when the user is not permitted to edit, like they are hidden on the list pages already (#2814) ### Fixes - Resolved issue where workflow voting could not be removed, which caused UI to display raw translation keys due to nil voting values. (#3357) From c8cbf18b3519a9015789e7a01f863bc02f3cf4af Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Thu, 6 Nov 2025 13:01:12 +0200 Subject: [PATCH 31/80] Hide `Manage categories` along with other edit functionality --- src/cljs/rems/administration/catalogue_item.cljs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cljs/rems/administration/catalogue_item.cljs b/src/cljs/rems/administration/catalogue_item.cljs index e17ba4b812..eac15b0329 100644 --- a/src/cljs/rems/administration/catalogue_item.cljs +++ b/src/cljs/rems/administration/catalogue_item.cljs @@ -107,8 +107,8 @@ [roles/show-when-can-modify-organization-item catalogue-item [edit-button id] [perform-action-button (toggle-enabled catalogue-item)] - [perform-action-button (toggle-archived catalogue-item)]] - [manage-categories-button]])]) + [perform-action-button (toggle-archived catalogue-item)] + [manage-categories-button]]])]) (defn catalogue-item-page [] (let [catalogue-item (rf/subscribe [::catalogue-item]) From f3e7708f15a9d63900ae3abde001325b1a331a9a Mon Sep 17 00:00:00 2001 From: Matias Vuorio Date: Wed, 12 Nov 2025 14:03:35 +0200 Subject: [PATCH 32/80] Remove en dash if title is empty --- src/cljs/rems/atoms.cljs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) mode change 100644 => 100755 src/cljs/rems/atoms.cljs diff --git a/src/cljs/rems/atoms.cljs b/src/cljs/rems/atoms.cljs old mode 100644 new mode 100755 index a81773a914..f8de40206b --- a/src/cljs/rems/atoms.cljs +++ b/src/cljs/rems/atoms.cljs @@ -222,10 +222,11 @@ " <" (:email email) ">")])) (defn- set-document-title! [s] - (r/with-let [title (r/reaction ; changes when text and text-format change - (if-not (str/blank? s) - (text-format :t.label/dash s (text :t.header/title)) - (text :t.header/title)))] + (r/with-let [app-title (text :t.header/title) + title (r/reaction ; changes when text and text-format change + (if-not (str/blank? app-title) + (text-format :t.label/dash s app-title) + s))] (set! (.-title js/document) @title))) (defn document-title [title & [{:keys [heading?] :or {heading? true}}]] From 7c65afa4725aa872a33fc2e098139f13a2e840cf Mon Sep 17 00:00:00 2001 From: Matias Vuorio Date: Thu, 13 Nov 2025 15:38:09 +0200 Subject: [PATCH 33/80] Remove en dash if title is empty --- src/cljs/rems/atoms.cljs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/cljs/rems/atoms.cljs b/src/cljs/rems/atoms.cljs index f8de40206b..a81773a914 100755 --- a/src/cljs/rems/atoms.cljs +++ b/src/cljs/rems/atoms.cljs @@ -222,11 +222,10 @@ " <" (:email email) ">")])) (defn- set-document-title! [s] - (r/with-let [app-title (text :t.header/title) - title (r/reaction ; changes when text and text-format change - (if-not (str/blank? app-title) - (text-format :t.label/dash s app-title) - s))] + (r/with-let [title (r/reaction ; changes when text and text-format change + (if-not (str/blank? s) + (text-format :t.label/dash s (text :t.header/title)) + (text :t.header/title)))] (set! (.-title js/document) @title))) (defn document-title [title & [{:keys [heading?] :or {heading? true}}]] From fef8295902775c0ab78e2dbb7419317892e38f06 Mon Sep 17 00:00:00 2001 From: Matias Vuorio Date: Thu, 13 Nov 2025 15:45:06 +0200 Subject: [PATCH 34/80] Remove en dash if title is empty --- src/cljs/rems/atoms.cljs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/cljs/rems/atoms.cljs b/src/cljs/rems/atoms.cljs index a81773a914..5432d5a33c 100755 --- a/src/cljs/rems/atoms.cljs +++ b/src/cljs/rems/atoms.cljs @@ -221,12 +221,15 @@ (str (localized (:name email)) " <" (:email email) ">")])) -(defn- set-document-title! [s] - (r/with-let [title (r/reaction ; changes when text and text-format change - (if-not (str/blank? s) - (text-format :t.label/dash s (text :t.header/title)) - (text :t.header/title)))] - (set! (.-title js/document) @title))) +(defn- set-document-title! [title] + (r/with-let [app-title (text :t.header/title) + document-title (r/reaction ; changes when text and text-format change + (cond + (and (str/blank? app-title) (str/blank? title)) "REMS" + (and (not (str/blank? app-title)) (str/blank? title)) app-title + (and (str/blank? app-title) (not (str/blank? title))) title + :else (text-format :t.label/dash title app-title)))] + (set! (.-title js/document) @document-title))) (defn document-title [title & [{:keys [heading?] :or {heading? true}}]] (let [the-title @(r/track set-document-title! title)] From 55e060630e045621672199ebf29cd12177861f7b Mon Sep 17 00:00:00 2001 From: Matias Vuorio Date: Thu, 13 Nov 2025 16:02:37 +0200 Subject: [PATCH 35/80] CHANGELOG.md: Remove en dash if title is empty --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f3ccf883f..c6159d94af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Changes since v2.38.1 ### Fixes - Resolved issue where workflow voting could not be removed, which caused UI to display raw translation keys due to nil voting values. (#3357) - Fixed an issue with the application page where the 'Add member' dropdown menu displayed members that already exist in the application. (#1412) +- Document title no longer shows an extra em dash when the localized application title is empty. ## v2.38.1 "Välimerenkatu +1" 2025-01-27 From 9c2a7b7f017f51804faa2e1258a1d8458c086028 Mon Sep 17 00:00:00 2001 From: Matias Vuorio Date: Mon, 17 Nov 2025 19:31:08 +0200 Subject: [PATCH 36/80] Remove en dash if title is empty --- dev-config.edn | 3 ++- src/cljs/rems/atoms.cljs | 15 +++++++-------- test-config.edn | 3 ++- test/clj/rems/test_browser.clj | 6 +++--- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/dev-config.edn b/dev-config.edn index 5c0968a457..70dfa76e0f 100644 --- a/dev-config.edn +++ b/dev-config.edn @@ -56,7 +56,8 @@ :heading false :translations {:fi {:title "Mixed" :filename "mixed-fi.md"} - :en {:url "https://example.org/en/mixed"}} ; missing sv and da + :en {:title "Mixed" + :url "https://example.org/en/mixed"}} ; missing sv and da :show-menu false :show-footer false} {:id "unlocalized" diff --git a/src/cljs/rems/atoms.cljs b/src/cljs/rems/atoms.cljs index 5432d5a33c..4099064b22 100755 --- a/src/cljs/rems/atoms.cljs +++ b/src/cljs/rems/atoms.cljs @@ -221,15 +221,14 @@ (str (localized (:name email)) " <" (:email email) ">")])) -(defn- set-document-title! [title] +(defn- set-document-title! [s] (r/with-let [app-title (text :t.header/title) - document-title (r/reaction ; changes when text and text-format change - (cond - (and (str/blank? app-title) (str/blank? title)) "REMS" - (and (not (str/blank? app-title)) (str/blank? title)) app-title - (and (str/blank? app-title) (not (str/blank? title))) title - :else (text-format :t.label/dash title app-title)))] - (set! (.-title js/document) @document-title))) + title (r/reaction ; changes when text and text-format change + (if-not (str/blank? app-title) + (text-format :t.label/dash s app-title) + s))] + (set! (.-title js/document) @title))) + (defn document-title [title & [{:keys [heading?] :or {heading? true}}]] (let [the-title @(r/track set-document-title! title)] diff --git a/test-config.edn b/test-config.edn index e5b816d6af..dcc52ce84f 100644 --- a/test-config.edn +++ b/test-config.edn @@ -42,7 +42,8 @@ :heading false :translations {:fi {:title "Mixed" :filename "mixed-fi.md"} - :en {:url "https://example.org/en/mixed"}} ; missing sv + :en {:title "Mixed" + :url "https://example.org/en/mixed"}} ; missing sv :show-menu false :show-footer false} {:id "unlocalized" diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj index 24a14df162..81187874be 100644 --- a/test/clj/rems/test_browser.clj +++ b/test/clj/rems/test_browser.clj @@ -1671,7 +1671,7 @@ (is (btu/eventually-visible? {:fn/has-text "Joining the application failed"})) (btu/screenshot "someone-else-cannot-use-the-link"))))) -(deftest test-approve-with-end-date +#_(deftest test-approve-with-end-date (testing "submit test data with API" (btu/context-assoc! :form-id (test-helpers/create-form! {:form/fields [{:field/title {:en "description" :fi "kuvaus" :sv "rubrik"} :field/optional false @@ -2162,7 +2162,7 @@ "Active" true} (dissoc (slurp-fields :catalogue-item) "Start"))))))) -(deftest test-update-catalogue-item +#_(deftest test-update-catalogue-item (btu/with-postmortem (btu/context-assoc! :workflow1 (test-helpers/create-workflow! {:title "test-update-catalogue-item workflow 1"})) (btu/context-assoc! :workflow2 (test-helpers/create-workflow! {:title "test-update-catalogue-item workflow 2"})) @@ -3680,7 +3680,7 @@ (btu/go (str (btu/get-server-url) "extra-pages/mixed")) ;; no header, only default translation in document title (is (not (btu/visible? {:tag :h1}))) - (is (= "REMS" (btu/get-title))) + (is (= "Mixed – REMS" (btu/get-title))) (is (btu/eventually-visible? {:tag :a :fn/has-text "https://example.org/en/mixed"})))) (btu/go (btu/get-server-url)) From a2ecec536e49bcfdb111d6c004fa6489bd3732fd Mon Sep 17 00:00:00 2001 From: Matias Vuorio Date: Mon, 17 Nov 2025 19:33:41 +0200 Subject: [PATCH 37/80] Remove en dash if title is empty --- src/cljs/rems/atoms.cljs | 1 - test/clj/rems/test_browser.clj | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/cljs/rems/atoms.cljs b/src/cljs/rems/atoms.cljs index 4099064b22..76e0354d54 100755 --- a/src/cljs/rems/atoms.cljs +++ b/src/cljs/rems/atoms.cljs @@ -229,7 +229,6 @@ s))] (set! (.-title js/document) @title))) - (defn document-title [title & [{:keys [heading?] :or {heading? true}}]] (let [the-title @(r/track set-document-title! title)] (when heading? diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj index 81187874be..504de97800 100644 --- a/test/clj/rems/test_browser.clj +++ b/test/clj/rems/test_browser.clj @@ -1671,7 +1671,7 @@ (is (btu/eventually-visible? {:fn/has-text "Joining the application failed"})) (btu/screenshot "someone-else-cannot-use-the-link"))))) -#_(deftest test-approve-with-end-date +(deftest test-approve-with-end-date (testing "submit test data with API" (btu/context-assoc! :form-id (test-helpers/create-form! {:form/fields [{:field/title {:en "description" :fi "kuvaus" :sv "rubrik"} :field/optional false @@ -2162,7 +2162,7 @@ "Active" true} (dissoc (slurp-fields :catalogue-item) "Start"))))))) -#_(deftest test-update-catalogue-item +(deftest test-update-catalogue-item (btu/with-postmortem (btu/context-assoc! :workflow1 (test-helpers/create-workflow! {:title "test-update-catalogue-item workflow 1"})) (btu/context-assoc! :workflow2 (test-helpers/create-workflow! {:title "test-update-catalogue-item workflow 2"})) From acbdc1f2b751a39c1cfd9a275cd9c23486964f1a Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Thu, 13 Nov 2025 16:34:49 +0200 Subject: [PATCH 38/80] test: when editing an organization without being the owner, edit button is hidden --- test/clj/rems/test_browser.clj | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj index 24a14df162..36e9c6f28d 100644 --- a/test/clj/rems/test_browser.clj +++ b/test/clj/rems/test_browser.clj @@ -3291,7 +3291,14 @@ (->> (slurp-table :organizations) (some #(when (= "SNEN" (get % "short-name")) (get % "commands"))))) - "organization actions should not be visible for non organization owner")))))))) + "organization actions should not be visible for non organization owner") + (testing "edit button is not visible when not organization owner" + (click-row-action [:organizations] + {:fn/text (str (btu/context-getx :organization-name) " EN")} + (select-button-by-label "View")) + (btu/wait-page-loaded) + (is (btu/eventually-visible? :back)) + (is (not (btu/visible? :edit))))))))))) (deftest test-small-navbar (testing "create a test application with the API to have another page to navigate to" From b4d34039824a3a8d7769dca8cbe04971a5458dc4 Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Mon, 17 Nov 2025 17:37:06 +0200 Subject: [PATCH 39/80] test: when editing a license without being the owner, edit button is hidden --- test/clj/rems/test_browser.clj | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj index 36e9c6f28d..da1a2379a0 100644 --- a/test/clj/rems/test_browser.clj +++ b/test/clj/rems/test_browser.clj @@ -3608,7 +3608,39 @@ "Bilaga (FI)" "Ladda ner fil\nE2E license with attachments (FI)" "Bilaga (SV)" "Ladda ner fil\nE2E license with attachments (SV)" "Aktiv" true} - (slurp-fields :license)))))))) + (slurp-fields :license)))))) + (testing "edit buttons are not visible when not organization owner" + (with-change-language :en + (test-helpers/create-organization! {:actor "owner" + :organization/id "other-organization" + :organization/short-name {:en "dummy-en" :fi "dummy-fi" :sv "dummy-sv"} + :organization/name {:en "dummy-organization-en" + :fi "dummy-organization-fi" + :sv "dummy-organization-sv"} + :organization/owners [{:userid "organization-owner1"}]}) + (test-helpers/create-license! {:actor "owner" + :organization {:organization/id "other-organization"} + :license/title {:en "License-EN" + :fi "License-FI" + :sv "License-SV"} + :license/text {:en "License text EN" + :fi "License text FI" + :sv "License text SV"}}) + (logout) + (login-as "organization-owner2") + (go-to-admin "Licenses") + (btu/scroll-and-click {:fn/text "Own organization only"}) + (btu/wait-visible [:licenses {:fn/text "License-EN"}]) + (is (->> (slurp-table :licenses) + (some #(when (= "dummy-en" (get % "organization")) + (get % "commands"))) + (= "View"))) + (click-row-action [:licenses] + {:fn/text "dummy-en"} + (select-button-by-label "View")) + (is (btu/eventually-visible? :back)) + (is (not (btu/visible? :disable))) + (is (not (btu/visible? :archive))))))) (deftest test-extra-pages (btu/with-postmortem From bdf8d8c8cad7ed5f5d7def9d17d9883b6014df19 Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Tue, 18 Nov 2025 17:13:43 +0200 Subject: [PATCH 40/80] Simplify test by using the default organization Also, assert that the license from default organization is not visible to org-owner2 before unticking `Own organization only` --- test/clj/rems/test_browser.clj | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj index da1a2379a0..71ee65c022 100644 --- a/test/clj/rems/test_browser.clj +++ b/test/clj/rems/test_browser.clj @@ -3611,16 +3611,7 @@ (slurp-fields :license)))))) (testing "edit buttons are not visible when not organization owner" (with-change-language :en - (test-helpers/create-organization! {:actor "owner" - :organization/id "other-organization" - :organization/short-name {:en "dummy-en" :fi "dummy-fi" :sv "dummy-sv"} - :organization/name {:en "dummy-organization-en" - :fi "dummy-organization-fi" - :sv "dummy-organization-sv"} - :organization/owners [{:userid "organization-owner1"}]}) - (test-helpers/create-license! {:actor "owner" - :organization {:organization/id "other-organization"} - :license/title {:en "License-EN" + (test-helpers/create-license! {:license/title {:en "License-EN" :fi "License-FI" :sv "License-SV"} :license/text {:en "License text EN" @@ -3629,14 +3620,16 @@ (logout) (login-as "organization-owner2") (go-to-admin "Licenses") + (btu/wait-visible [:licenses {:fn/text "E2E license with external links (EN)"}]) + (is (not (btu/visible? [:licenses {:fn/text "License-EN"}]))) (btu/scroll-and-click {:fn/text "Own organization only"}) (btu/wait-visible [:licenses {:fn/text "License-EN"}]) (is (->> (slurp-table :licenses) - (some #(when (= "dummy-en" (get % "organization")) + (some #(when (= "Default" (get % "organization")) (get % "commands"))) (= "View"))) (click-row-action [:licenses] - {:fn/text "dummy-en"} + {:fn/text "Default"} (select-button-by-label "View")) (is (btu/eventually-visible? :back)) (is (not (btu/visible? :disable))) From f2bfbc30740effc19cbc6fa0e06a5784b407ee36 Mon Sep 17 00:00:00 2001 From: vuorioma Date: Tue, 18 Nov 2025 17:51:17 +0200 Subject: [PATCH 41/80] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6159d94af..32c27ad65d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ Changes since v2.38.1 ### Fixes - Resolved issue where workflow voting could not be removed, which caused UI to display raw translation keys due to nil voting values. (#3357) - Fixed an issue with the application page where the 'Add member' dropdown menu displayed members that already exist in the application. (#1412) -- Document title no longer shows an extra em dash when the localized application title is empty. +- Document title no longer shows an extra em dash when the localized application title is empty. (#3398) ## v2.38.1 "Välimerenkatu +1" 2025-01-27 From 2f26f04a6ea7a172c0799c76c22d4f6198aa1c66 Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Wed, 19 Nov 2025 15:59:57 +0200 Subject: [PATCH 42/80] test: when viewing catalogue items as owner of another organization, edit functionality is hidden --- test/clj/rems/test_browser.clj | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj index 71ee65c022..c60effdee7 100644 --- a/test/clj/rems/test_browser.clj +++ b/test/clj/rems/test_browser.clj @@ -2270,7 +2270,33 @@ "active" true "organization" "Default"}] (->> (slurp-tds [:catalogue {:css "tr:has(td.selection *[aria-checked=true])"}]) - (mapv #(dissoc % "resource" "created" "commands")))))))) + (mapv #(dissoc % "resource" "created" "commands")))))) + + (testing "modify actions are not enabled when not organization owner" + (logout) + (login-as "organization-owner2") + (go-to-admin "Catalogue items") + (btu/wait-page-loaded) + (is (not (btu/visible? {:fn/text "test-update-catalogue-item 1 EN"}))) + (is (not (btu/visible? {:fn/text "test-update-catalogue-item 2 EN"}))) + (is (not (btu/visible? {:fn/text "test-update-catalogue-item 3 EN"}))) + (btu/scroll-and-click {:fn/text "Own organization only"}) + (is (btu/eventually-visible? {:fn/text "test-update-catalogue-item 1 EN"})) + (is (btu/eventually-visible? {:fn/text "test-update-catalogue-item 2 EN"})) + (is (btu/eventually-visible? {:fn/text "test-update-catalogue-item 3 EN"})) + (is (->> (slurp-rows :catalogue) + (filter #(= "Default" (get % "organization"))) + (every? #(= "View" (get % "commands")))))) + + (testing "edit buttons are not visible" + (click-row-action [:catalogue] + {:fn/text "test-update-catalogue-item 1 EN"} + (select-button-by-label "View")) + (is (btu/eventually-visible? :back)) + (is (not (btu/visible? :edit))) + (is (not (btu/visible? :disable))) + (is (not (btu/visible? :archive))) + (is (not (btu/visible? :manage-categories)))))) ;;; form editor test utilities From bd4018b8a1dd304ddc7b6d92886d499a35c224ef Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Thu, 20 Nov 2025 13:00:22 +0200 Subject: [PATCH 43/80] test: when viewing workflows as owner of another organization, edit functionality is hidden --- test/clj/rems/test_browser.clj | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj index c60effdee7..242ee10a8b 100644 --- a/test/clj/rems/test_browser.clj +++ b/test/clj/rems/test_browser.clj @@ -2939,6 +2939,7 @@ (btu/with-postmortem (login-as "owner") (go-to-admin "Workflows") + (testing "create workflow" (btu/context-assoc! :workflow-title (str "test-workflow-create-edit " (btu/get-seed))) (btu/scroll-and-click :create-workflow) @@ -2956,6 +2957,7 @@ (select-option "Licenses" "General Terms of Use") (btu/screenshot "test-workflow-create-edit-1") (btu/scroll-and-click :save)) + (testing "view workflow" (wait-page-title "Workflow – REMS") (btu/wait-page-loaded) @@ -2983,6 +2985,7 @@ (is (= "General Terms of Use" (btu/get-element-text {:tag :div :id :workflow-licenses}))) ; readonly field (btu/screenshot "test-workflow-create-edit-4") (btu/scroll-and-click :save)) + (testing "view workflow again" (wait-page-title "Workflow – REMS") (btu/wait-page-loaded) @@ -2996,7 +2999,27 @@ "Licenses" "General Terms of Use" "Active" true} (slurp-fields :workflow-common-fields))) - (is (btu/visible? {:tag :a :fn/text "Simple form"}))))) + (is (btu/visible? {:tag :a :fn/text "Simple form"}))) + + (testing "edit buttons are not visible when not organization owner" + (logout) + (login-as "organization-owner2") + (go-to-admin "Workflows") + (btu/wait-page-loaded) + (is (not (btu/visible? {:fn/text (str (btu/context-getx :workflow-title) " v2")}))) + (btu/scroll-and-click {:fn/text "Own organization only"}) + (btu/eventually-visible? {:fn/text (str (btu/context-getx :workflow-title) " v2")}) + (is (->> (slurp-table :workflows) + (some #(when (= "Default" (get % "organization")) + (get % "commands"))) + (= "View"))) + (click-row-action [:workflows] + {:fn/text (str (btu/context-getx :workflow-title) " v2")} + (select-button-by-label "View")) + (is (btu/eventually-visible? :back)) + (is (not (btu/visible? :edit))) + (is (not (btu/visible? :disable))) + (is (not (btu/visible? :archive)))))) (deftest test-blacklist (btu/with-postmortem From 418cbb2e5fccabefc8cef0ea719ad1bd932d9ea2 Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Thu, 20 Nov 2025 13:59:55 +0200 Subject: [PATCH 44/80] Wait until form field is visible Prevent Chromedriver error `no such element` when test execution is faster than what rendering can keep up with. --- test/clj/rems/test_browser.clj | 1 + 1 file changed, 1 insertion(+) diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj index 242ee10a8b..842f0562f6 100644 --- a/test/clj/rems/test_browser.clj +++ b/test/clj/rems/test_browser.clj @@ -2469,6 +2469,7 @@ :let [column-id (keyword (format "columns-%s-key" i)) label-id (comp keyword (partial format "columns-%s-label-%s" i) name)]] (btu/scroll-and-click (field-component :add-column)) + (btu/wait-visible (field-component column-id)) (btu/fill-human (field-component column-id) (format "table-column-%s" i)) (btu/fill-human (field-component (label-id :en)) (format "Table column %s (EN)" i)) (btu/fill-human (field-component (label-id :fi)) (format "Table column %s (FI)" i)) From b39b96ea52ea8cbeb00d883dcedb0d3c1fa3ad73 Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Thu, 20 Nov 2025 15:00:34 +0200 Subject: [PATCH 45/80] test: when viewing forms as owner of another organization, edit functionality is hidden --- test/clj/rems/test_browser.clj | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj index 842f0562f6..b210cd6685 100644 --- a/test/clj/rems/test_browser.clj +++ b/test/clj/rems/test_browser.clj @@ -2360,7 +2360,7 @@ (testing "create form" (btu/scroll-and-click {:fn/text "Create form"}) (wait-page-title "Create form – REMS") - (select-option "Organization" "nbn") + (select-option "Organization" "Default") (fill-form-field "Name" "Form editor test") (fill-form-field "EN" "Form Editor Test (EN)") (fill-form-field "FI" "Form Editor Test (FI)") @@ -2552,7 +2552,7 @@ (testing "view form" (wait-page-title "Form – REMS") (btu/wait-page-loaded) - (is (= {"Organization" "NBN" + (is (= {"Organization" "The Default Organization" "Name" "Form editor test" "Title (EN)" "Form Editor Test (EN)" "Title (FI)" "Form Editor Test (FI)" @@ -2646,10 +2646,32 @@ (btu/wait-page-loaded) (wait-page-title "Form – REMS"))) + (testing "edit buttons are not visible when not organization owner" + (logout) + (login-as "organization-owner2") + (go-to-admin "Forms") + (btu/wait-page-loaded) + (btu/scroll-query :forms) + (is (not (btu/visible? {:fn/text "Form editor test"}))) + (btu/scroll-and-click {:fn/text "Own organization only"}) + (btu/eventually-visible? {:fn/text "Form editor test"}) + (is (= "View\nCopy as new" + (->> (slurp-table :forms) + (some #(when (= "Default" (get % "organization")) + (get % "commands")))))) + (click-row-action [:forms] + {:fn/text "Form editor test"} + (select-button-by-label "View")) + (is (btu/eventually-visible? :back)) + (is (btu/visible? {:fn/has-class :btn :fn/has-text "Copy as new"})) + (is (not (btu/visible? :edit))) + (is (not (btu/visible? :disable))) + (is (not (btu/visible? :archive)))) + (testing "fetch form via api" (let [form-id (Integer/parseInt (last (str/split (btu/get-url) #"/")))] (is (= {:form/id form-id - :organization {:organization/id "nbn" :organization/name {:fi "NBN" :en "NBN" :sv "NBN"} :organization/short-name {:fi "NBN" :en "NBN" :sv "NBN"}} + :organization {:organization/id "default" :organization/name {:fi "Oletusorganisaatio" :en "The Default Organization" :sv "Standardorganisationen"} :organization/short-name {:fi "Oletus" :en "Default" :sv "Standard"}} :form/internal-name "Form editor test" :form/external-title {:en "Form Editor Test (EN)" :fi "Form Editor Test (FI)" From d789e5a4e246d0bde009bf4451a9ce6e88f5cd4a Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Thu, 20 Nov 2025 15:06:41 +0200 Subject: [PATCH 46/80] Clarify test output by unthreading `(= expected actual)` --- test/clj/rems/test_browser.clj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj index b210cd6685..3533aed418 100644 --- a/test/clj/rems/test_browser.clj +++ b/test/clj/rems/test_browser.clj @@ -3032,10 +3032,10 @@ (is (not (btu/visible? {:fn/text (str (btu/context-getx :workflow-title) " v2")}))) (btu/scroll-and-click {:fn/text "Own organization only"}) (btu/eventually-visible? {:fn/text (str (btu/context-getx :workflow-title) " v2")}) - (is (->> (slurp-table :workflows) - (some #(when (= "Default" (get % "organization")) - (get % "commands"))) - (= "View"))) + (is (= "View" + (->> (slurp-table :workflows) + (some #(when (= "Default" (get % "organization")) + (get % "commands")))))) (click-row-action [:workflows] {:fn/text (str (btu/context-getx :workflow-title) " v2")} (select-button-by-label "View")) @@ -3696,10 +3696,10 @@ (is (not (btu/visible? [:licenses {:fn/text "License-EN"}]))) (btu/scroll-and-click {:fn/text "Own organization only"}) (btu/wait-visible [:licenses {:fn/text "License-EN"}]) - (is (->> (slurp-table :licenses) - (some #(when (= "Default" (get % "organization")) - (get % "commands"))) - (= "View"))) + (is (= "View" + (->> (slurp-table :licenses) + (some #(when (= "Default" (get % "organization")) + (get % "commands")))))) (click-row-action [:licenses] {:fn/text "Default"} (select-button-by-label "View")) From 6c5dc8c44c23ebf2049393b072c01e32b8fe7367 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:17:29 +0000 Subject: [PATCH 47/80] chore(deps-dev): bump cipher-base from 1.0.4 to 1.0.6 Bumps [cipher-base](https://github.com/crypto-browserify/cipher-base) from 1.0.4 to 1.0.6. - [Changelog](https://github.com/browserify/cipher-base/blob/master/CHANGELOG.md) - [Commits](https://github.com/crypto-browserify/cipher-base/compare/v1.0.4...v1.0.6) --- updated-dependencies: - dependency-name: cipher-base dependency-version: 1.0.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index c05c018d94..f52494e455 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1144,13 +1144,18 @@ } }, "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", "dev": true, + "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" } }, "node_modules/cliui": { @@ -3665,9 +3670,9 @@ "dev": true }, "node_modules/to-buffer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", - "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", "dev": true, "license": "MIT", "dependencies": { From 343680e45d74f1b2546c2227b0f730c6c902c509 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:30:56 +0000 Subject: [PATCH 48/80] chore(deps-dev): bump sha.js from 2.4.11 to 2.4.12 Bumps [sha.js](https://github.com/crypto-browserify/sha.js) from 2.4.11 to 2.4.12. - [Changelog](https://github.com/browserify/sha.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/crypto-browserify/sha.js/compare/v2.4.11...v2.4.12) --- updated-dependencies: - dependency-name: sha.js dependency-version: 2.4.12 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index f52494e455..3cbd2ff202 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3332,16 +3332,24 @@ "dev": true }, "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "dev": true, + "license": "(MIT AND BSD-3-Clause)", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/shadow-cljs": { From 44548acb77f98426585b541d8cee6fb679ca352d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:02:12 +0000 Subject: [PATCH 49/80] chore(deps): bump tmp from 0.2.1 to 0.2.5 Bumps [tmp](https://github.com/raszi/node-tmp) from 0.2.1 to 0.2.5. - [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md) - [Commits](https://github.com/raszi/node-tmp/compare/v0.2.1...v0.2.5) --- updated-dependencies: - dependency-name: tmp dependency-version: 0.2.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3cbd2ff202..cf8a61aa2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3660,15 +3660,13 @@ } }, "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8.17.0" + "node": ">=14.14" } }, "node_modules/to-arraybuffer": { From 2411fd62ee8d59634c0d05a80d6b71fb4ffc3155 Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Mon, 24 Nov 2025 12:21:00 +0200 Subject: [PATCH 50/80] Disable `Update catalogue item` button when user is not permitted to modify a selected item --- src/cljs/rems/administration/catalogue_items.cljs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cljs/rems/administration/catalogue_items.cljs b/src/cljs/rems/administration/catalogue_items.cljs index 54aa920769..d2bfa0c532 100644 --- a/src/cljs/rems/administration/catalogue_items.cljs +++ b/src/cljs/rems/administration/catalogue_items.cljs @@ -87,7 +87,8 @@ [atoms/rate-limited-action-button {:id :update-catalogue-item :class "btn-primary" - :disabled (when (empty? items) :disabled) + :disabled (when (or (empty? items) + (some (complement roles/can-modify-organization-item?) items)) :disabled) :on-click (fn [] (rf/dispatch [:rems.administration.update-catalogue-item/enter-page items]) (navigate! "/administration/catalogue-items/update-catalogue-item")) From 0659206da2627b700c87d2f86ac6d8a5de136637 Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Mon, 24 Nov 2025 17:23:55 +0200 Subject: [PATCH 51/80] test: edit functionality is hidden when viewing catalogue items as owner of another organization --- test/clj/rems/test_browser.clj | 39 ++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj index 3533aed418..1aecdcb641 100644 --- a/test/clj/rems/test_browser.clj +++ b/test/clj/rems/test_browser.clj @@ -2167,11 +2167,15 @@ (btu/context-assoc! :workflow1 (test-helpers/create-workflow! {:title "test-update-catalogue-item workflow 1"})) (btu/context-assoc! :workflow2 (test-helpers/create-workflow! {:title "test-update-catalogue-item workflow 2"})) (btu/context-assoc! :workflow3 (test-helpers/create-workflow! {:title "test-update-catalogue-item workflow 3"})) + (btu/context-assoc! :workflow4 (test-helpers/create-workflow! {:title "test-update-catalogue-item workflow 4" + :organization {:organization/id "nbn"}})) (btu/context-assoc! :form1 (test-helpers/create-form! {:form/internal-name "test-update-catalogue-item form 1" :form/title {:en "test-update-catalogue-item form 1 EN" :fi "test-update-catalogue-item form 1 FI" :sv "test-update-catalogue-item form 1 SV"}})) (btu/context-assoc! :form2 (test-helpers/create-form! {:form/internal-name "test-update-catalogue-item form 2"})) + (btu/context-assoc! :form4 (test-helpers/create-form! {:form/internal-name "test-update-catalogue-item form 4" + :organization {:organization/id "nbn"}})) (btu/context-assoc! :catalogue-item1 (test-helpers/create-catalogue-item! {:title {:en "test-update-catalogue-item 1 EN" :fi "test-update-catalogue-item 1 FI" :sv "test-update-catalogue-item 1 SV"} @@ -2187,6 +2191,12 @@ :sv "test-update-catalogue-item 3 SV"} :form-id (btu/context-getx :form2) :workflow-id (btu/context-getx :workflow2)})) + (btu/context-assoc! :catalogue-item4 (test-helpers/create-catalogue-item! {:title {:en "test-update-catalogue-item 4 EN" + :fi "test-update-catalogue-item 4 FI" + :sv "test-update-catalogue-item 4 SV"} + :form-id (btu/context-getx :form4) + :workflow-id (btu/context-getx :workflow4) + :organization {:organization/id "nbn"}})) (login-as "owner") (go-to-admin "Catalogue items") (btu/wait-page-loaded) @@ -2280,6 +2290,7 @@ (is (not (btu/visible? {:fn/text "test-update-catalogue-item 1 EN"}))) (is (not (btu/visible? {:fn/text "test-update-catalogue-item 2 EN"}))) (is (not (btu/visible? {:fn/text "test-update-catalogue-item 3 EN"}))) + (is (btu/eventually-visible? {:fn/text "test-update-catalogue-item 4 EN"})) (btu/scroll-and-click {:fn/text "Own organization only"}) (is (btu/eventually-visible? {:fn/text "test-update-catalogue-item 1 EN"})) (is (btu/eventually-visible? {:fn/text "test-update-catalogue-item 2 EN"})) @@ -2288,7 +2299,35 @@ (filter #(= "Default" (get % "organization"))) (every? #(= "View" (get % "commands")))))) + (testing "update is disabled when not owner of selected items" + (btu/wait-disabled {:tag :button :fn/text "Update catalogue item"}) + (btu/scroll-and-click {:fn/text "test-update-catalogue-item 4 EN"}) + (btu/scroll-and-click {:fn/text "test-update-catalogue-item 1 EN"}) + (btu/wait-disabled {:tag :button :fn/text "Update catalogue item"}) + (btu/scroll-and-click {:fn/text "test-update-catalogue-item 1 EN"}) + (btu/wait-enabled {:tag :button :fn/text "Update catalogue item"}) + + (testing "update remains disabled when selecting all items with toggle-all" + (btu/scroll-and-click {:id ":rems.administration.catalogue-items/catalogue-selection-toggle-all"}) + (btu/wait-page-loaded) + (is (empty? (slurp-tds [:catalogue {:css "tr:has(td.selection *[aria-checked=true])"}]))) + (btu/scroll-and-click {:id ":rems.administration.catalogue-items/catalogue-selection-toggle-all"}) + (btu/wait-page-loaded) + (is (not-empty (slurp-tds [:catalogue {:css "tr:has(td.selection *[aria-checked=true])"}]))) + (btu/wait-disabled {:tag :button :fn/text "Update catalogue item"})) + + (testing "update is enabled when selecting all items under the user's organization with toggle-all" + (btu/scroll-and-click {:id ":rems.administration.catalogue-items/catalogue-selection-toggle-all"}) + (btu/scroll-and-click {:fn/text "Own organization only"}) + (btu/scroll-and-click {:id ":rems.administration.catalogue-items/catalogue-selection-toggle-all"}) + (btu/wait-page-loaded) + (is (= ["test-update-catalogue-item 4 EN"] + (mapv #(get % "name") + (slurp-tds [:catalogue {:css "tr:has(td.selection *[aria-checked=true])"}])))) + (btu/wait-enabled {:tag :button :fn/text "Update catalogue item"}))) + (testing "edit buttons are not visible" + (btu/scroll-and-click {:fn/text "Own organization only"}) (click-row-action [:catalogue] {:fn/text "test-update-catalogue-item 1 EN"} (select-button-by-label "View")) From 204c6150ee7d9eff2d24b946f4f5965cbd5c44d6 Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Fri, 28 Nov 2025 14:44:15 +0200 Subject: [PATCH 52/80] Add http-level test for audit log response status --- test/clj/rems/api/test_over_http.clj | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/clj/rems/api/test_over_http.clj b/test/clj/rems/api/test_over_http.clj index 45883c5ac0..10553596fc 100644 --- a/test/clj/rems/api/test_over_http.clj +++ b/test/clj/rems/api/test_over_http.clj @@ -119,3 +119,25 @@ :keys first :kid)))) + +(deftest test-api-audit-log + (create-test-data) + (test-helpers/create-user! {:userid "reporter"} :reporter) + + (testing "populate audit log with a 404 not found" + (is (thrown-with-msg? clojure.lang.ExceptionInfo + #"clj-http: status 404" + (http/get "http://localhost:3001/api/unknown")))) + + (testing "response status is 200 OK" + (let [{:keys [status body]} (http/get (str (:public-url rems.config/env) "/api/audit-log") + {:as :json + :headers {"x-rems-api-key" "42" + "x-rems-user-id" "reporter"}})] + (is (= 200 status)) + (is (= [{:apikey nil + :method "get" + :path "/api/unknown" + :status "404" + :userid nil}] + (mapv #(dissoc % :time) body)))))) From 7a109cb6241dcf35f6ed26caeda667652c7e4b23 Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Mon, 1 Dec 2025 09:36:32 +0200 Subject: [PATCH 53/80] Improve readability --- src/cljs/rems/administration/catalogue_items.cljs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cljs/rems/administration/catalogue_items.cljs b/src/cljs/rems/administration/catalogue_items.cljs index d2bfa0c532..cd1570877e 100644 --- a/src/cljs/rems/administration/catalogue_items.cljs +++ b/src/cljs/rems/administration/catalogue_items.cljs @@ -88,7 +88,8 @@ {:id :update-catalogue-item :class "btn-primary" :disabled (when (or (empty? items) - (some (complement roles/can-modify-organization-item?) items)) :disabled) + (not-every? roles/can-modify-organization-item? items)) + :disabled) :on-click (fn [] (rf/dispatch [:rems.administration.update-catalogue-item/enter-page items]) (navigate! "/administration/catalogue-items/update-catalogue-item")) From 3fb89d1b9e43b8785c1d1809df41ae47ea0cee51 Mon Sep 17 00:00:00 2001 From: Olli Pikarinen Date: Tue, 2 Dec 2025 16:22:09 +0200 Subject: [PATCH 54/80] Fix flaky test by waiting until page is loaded --- test/clj/rems/test_browser.clj | 1 + 1 file changed, 1 insertion(+) diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj index 4d74d82723..ceb5902db4 100644 --- a/test/clj/rems/test_browser.clj +++ b/test/clj/rems/test_browser.clj @@ -2329,6 +2329,7 @@ (testing "edit buttons are not visible" (btu/scroll-and-click {:fn/text "Own organization only"}) + (btu/wait-page-loaded) (click-row-action [:catalogue] {:fn/text "test-update-catalogue-item 1 EN"} (select-button-by-label "View")) From 429040844f72f8a658f52b33091a4bffa2a772bd Mon Sep 17 00:00:00 2001 From: Markku Rontu Date: Thu, 11 Dec 2025 10:57:17 +0200 Subject: [PATCH 55/80] fix: catalogue table row-commands The recent refactoring wrt. the catalogue tree accessibility had a small regression in that the row commands are visible even when not logged in. The check remains in the catalogue tree and we can easily add that back to the catalogue table too. --- src/cljs/rems/catalogue.cljs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cljs/rems/catalogue.cljs b/src/cljs/rems/catalogue.cljs index 654c5d2a3e..14663369d8 100644 --- a/src/cljs/rems/catalogue.cljs +++ b/src/cljs/rems/catalogue.cljs @@ -112,7 +112,8 @@ :name {:value (get-localized-title item)} :commands {:display-value [:div.commands.flex-nowrap.justify-content-end [catalogue-item-more-info item] - [row-command item cart-item-ids]]}}) + (when @roles/logged-in? + [row-command item cart-item-ids])]}}) catalogue)))) (defn draft-application-list [] From f918beb2668e07bae5d4a157a892d3379cc8a347 Mon Sep 17 00:00:00 2001 From: Markku Rontu Date: Thu, 11 Dec 2025 11:00:23 +0200 Subject: [PATCH 56/80] doc: update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d4679e040..20af988aff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Changes since v2.38.1 - Document title no longer shows an extra em dash when the localized application title is empty. (#3398) - Fix audit log API. Technical error from incorrect response. (#3380) - Fixes to enable JVM 25 support (https://github.com/tolitius/cprop/issues/60, https://clojure.atlassian.net/browse/CLJ-2764) +- Catalogue table had the add/remove cart commands even when not logged in. (#3408) ## v2.38.1 "Välimerenkatu +1" 2025-01-27 From bb791bb4c7d660341f786568784a3b5dd2a255ba Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Sun, 6 Jul 2025 21:31:02 +1000 Subject: [PATCH 57/80] docker-build --- .github/workflows/docker-build.yml | 46 ++++++++++++++++++++++++++++++ Dockerfile | 2 +- 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docker-build.yml diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000000..f574e8fb93 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,46 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - develop + - master + workflow_dispatch: + +permissions: + id-token: write + contents: read + +jobs: + build-and-push: + runs-on: ubuntu-latest + environment: dev + steps: + - uses: actions/checkout@v3 + + - name: Configure AWS credentials (Dev Account) + uses: aws-actions/configure-aws-credentials@v2 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN_DEV }} + aws-region: ${{ secrets.CDK_REGION_DEV }} + + - name: Log in to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ steps.login-ecr.outputs.registry }}/rems + tags: | + type=ref,event=branch + type=sha + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile index cfea60c4c9..e8681cde30 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,9 +9,9 @@ WORKDIR /rems ENTRYPOINT ["bash","./docker-entrypoint.sh"] +ADD https://github.com/CSCfi/rems/releases/download/v2.38.1/rems.jar /rems/rems.jar COPY empty-config.edn /rems/config/config.edn COPY example-theme/extra-styles.css /rems/example-theme/extra-styles.css -COPY target/uberjar/rems.jar /rems/rems.jar COPY docker-entrypoint.sh /rems/docker-entrypoint.sh RUN chmod 664 /opt/java/openjdk/lib/security/cacerts From 9e0604cebbe08461207e8e2b4ce475ae7108ab63 Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Mon, 7 Jul 2025 12:13:16 +1000 Subject: [PATCH 58/80] private keys --- docker-entrypoint.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index ec06b76eb3..811a178f5f 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,5 +1,8 @@ #!/bin/bash +echo "$PRIVATE_KEY" > /rems/keys/private-key.jwk +echo "$PUBLIC_KEY" > /rems/keys/public-key.jwk + certfile=$(ls /rems/certs 2>/dev/null) parameters=false cmd_prefix="" From fb93e31e8e318269a18373fe5f39fd2514ad9ab4 Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Mon, 7 Jul 2025 13:58:56 +1000 Subject: [PATCH 59/80] update entrypoint --- docker-entrypoint.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 811a178f5f..68d0ce5e3a 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -2,7 +2,9 @@ echo "$PRIVATE_KEY" > /rems/keys/private-key.jwk echo "$PUBLIC_KEY" > /rems/keys/public-key.jwk - +if [ -n "$REMS_CONFIG_EDN" ]; then + echo "$REMS_CONFIG_EDN" > /rems/config/config.edn +fi certfile=$(ls /rems/certs 2>/dev/null) parameters=false cmd_prefix="" From a17f0b7f1ab5966441863a6c9850e5aae356a7dc Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Tue, 8 Jul 2025 17:56:32 +1000 Subject: [PATCH 60/80] config template --- Dockerfile | 5 +++-- config.edn.template | 29 +++++++++++++++++++++++++++++ docker-entrypoint.sh | 8 +++++--- 3 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 config.edn.template diff --git a/Dockerfile b/Dockerfile index e8681cde30..5aab24e031 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,13 +4,14 @@ FROM eclipse-temurin:17-jre-alpine RUN apk add --no-cache bash -RUN mkdir /rems +RUN mkdir -p /rems/keys + WORKDIR /rems ENTRYPOINT ["bash","./docker-entrypoint.sh"] ADD https://github.com/CSCfi/rems/releases/download/v2.38.1/rems.jar /rems/rems.jar -COPY empty-config.edn /rems/config/config.edn +COPY config.edn.template /rems/config/config.edn.template COPY example-theme/extra-styles.css /rems/example-theme/extra-styles.css COPY docker-entrypoint.sh /rems/docker-entrypoint.sh diff --git a/config.edn.template b/config.edn.template new file mode 100644 index 0000000000..a6d5558045 --- /dev/null +++ b/config.edn.template @@ -0,0 +1,29 @@ +{:port 3000 + ;; Configure with deployed public URL so that OIDC callback can work + ;; :public-url "http://localhost:3000/" + :public-url "${PUBLIC_URL}" + :database-url "postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}?user=${DB_USER}&password=${DB_PASSWORD}" + :search-index-path "/tmp/rems-search-index" + :authentication :oidc + ;; Configure an OpenID Provider + :oidc-metadata-url "${OIDC_METADATA_URL}" + :oidc-client-id "${OIDC_CLIENT_ID}" + :oidc-client-secret "${OIDC_CLIENT_SECRET}" + :oidc-scopes "openid profile email" + :oidc-userid-attributes [{:attribute "sub"}] + :oidc-name-attributes ["name" "unique_name" "family_name"] + :oidc-email-attributes ["email"] + :log-authentication-details true + :languages [:en] + :catalogue-is-public true + :ga4gh-visa-private-key "/rems/keys/private-key.jwk" + :ga4gh-visa-public-key "/rems/keys/public-key.jwk" + :enable-cart true ; show shopping cart and allow bundling multiple resources into one application + :enable-permissions-api true + :enable-pdf-api true + :enable-catalogue-tree false ; might be useful for a lot of items + :catalogue-tree-show-matching-parents true + :enable-autosave true + :extra-stylesheets {:root "./" :files ["/theme/styles.css"]} + :theme-path "/rems/theme/theme.edn" + :extra-pages-path "/rems/extra-pages"} \ No newline at end of file diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 68d0ce5e3a..6898d0f1c5 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,10 +1,12 @@ #!/bin/bash +set -e echo "$PRIVATE_KEY" > /rems/keys/private-key.jwk echo "$PUBLIC_KEY" > /rems/keys/public-key.jwk -if [ -n "$REMS_CONFIG_EDN" ]; then - echo "$REMS_CONFIG_EDN" > /rems/config/config.edn -fi + +# Interpolate secrets and config into config.edn +envsubst < /rems/config/config.edn.template > /rems/config/config.edn + certfile=$(ls /rems/certs 2>/dev/null) parameters=false cmd_prefix="" From bda57135dfa6014263386663978b61fde846816b Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Tue, 8 Jul 2025 19:22:52 +1000 Subject: [PATCH 61/80] latest tag --- .github/workflows/docker-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index f574e8fb93..a1d219167d 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -36,6 +36,7 @@ jobs: tags: | type=ref,event=branch type=sha + ${{ github.ref == 'refs/heads/master' && 'latest' || '' }} - name: Build and push Docker image uses: docker/build-push-action@v5 From 3203e3f142587b7fdaf355eb3dcfa472f24eca60 Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Tue, 8 Jul 2025 19:48:27 +1000 Subject: [PATCH 62/80] envsubst --- Dockerfile | 2 +- config.edn.template | 8 ++++---- docker-entrypoint.sh | 27 ++++++++++++++++++++++++++- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5aab24e031..0518d614ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM eclipse-temurin:17-jre-alpine -RUN apk add --no-cache bash +RUN apk add --no-cache bash gettext RUN mkdir -p /rems/keys diff --git a/config.edn.template b/config.edn.template index a6d5558045..538aa8a86f 100644 --- a/config.edn.template +++ b/config.edn.template @@ -1,8 +1,7 @@ {:port 3000 ;; Configure with deployed public URL so that OIDC callback can work - ;; :public-url "http://localhost:3000/" :public-url "${PUBLIC_URL}" - :database-url "postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}?user=${DB_USER}&password=${DB_PASSWORD}" + :database-url "postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}?user=${DB_USER}&password=${DB_PASSWORD_ENCODED}" :search-index-path "/tmp/rems-search-index" :authentication :oidc ;; Configure an OpenID Provider @@ -25,5 +24,6 @@ :catalogue-tree-show-matching-parents true :enable-autosave true :extra-stylesheets {:root "./" :files ["/theme/styles.css"]} - :theme-path "/rems/theme/theme.edn" - :extra-pages-path "/rems/extra-pages"} \ No newline at end of file + ;; :theme-path "/rems/theme/theme.edn" + ;; :extra-pages-path "/rems/extra-pages" + } \ No newline at end of file diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 6898d0f1c5..8e05f2d6b3 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,5 +1,22 @@ #!/bin/bash -set -e +set -x + +# Encode db passwords with special characters +urlencode() { + local raw="$1" + local encoded="" + local i c + for (( i = 0; i < ${#raw}; i++ )); do + c="${raw:$i:1}" + case "$c" in + [a-zA-Z0-9.~_-]) encoded+="$c" ;; + *) encoded+=$(printf '%%%02X' "'$c") ;; + esac + done + echo "$encoded" +} + +export DB_PASSWORD_ENCODED=$(urlencode "$DB_PASSWORD") echo "$PRIVATE_KEY" > /rems/keys/private-key.jwk echo "$PUBLIC_KEY" > /rems/keys/public-key.jwk @@ -7,6 +24,14 @@ echo "$PUBLIC_KEY" > /rems/keys/public-key.jwk # Interpolate secrets and config into config.edn envsubst < /rems/config/config.edn.template > /rems/config/config.edn +echo "========================" +echo "Generated config.edn:" +cat /rems/config/config.edn +echo "========================" + +echo "########## RUNNING ONE-TIME MIGRATION ##########" +java -Drems.config=config/config.edn -jar rems.jar migrate + certfile=$(ls /rems/certs 2>/dev/null) parameters=false cmd_prefix="" From fa622b7bdabc45e81f83b5a1bd802789d380aae5 Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Mon, 14 Jul 2025 20:42:10 +1000 Subject: [PATCH 63/80] db migrate option --- docker-entrypoint.sh | 66 +++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 37 deletions(-) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 8e05f2d6b3..331ee35453 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,7 +1,8 @@ #!/bin/bash +set -euo pipefail set -x -# Encode db passwords with special characters +# Encode DB password to safely use in JDBC URL urlencode() { local raw="$1" local encoded="" @@ -18,10 +19,11 @@ urlencode() { export DB_PASSWORD_ENCODED=$(urlencode "$DB_PASSWORD") +# Write Visa key files echo "$PRIVATE_KEY" > /rems/keys/private-key.jwk echo "$PUBLIC_KEY" > /rems/keys/public-key.jwk -# Interpolate secrets and config into config.edn +# Generate config.edn envsubst < /rems/config/config.edn.template > /rems/config/config.edn echo "========================" @@ -29,45 +31,35 @@ echo "Generated config.edn:" cat /rems/config/config.edn echo "========================" -echo "########## RUNNING ONE-TIME MIGRATION ##########" -java -Drems.config=config/config.edn -jar rems.jar migrate +# Optional: install custom cert if provided +certfile=$(ls /rems/certs 2>/dev/null || true) +if [ -n "${certfile}" ] && [ "${certfile}" != "null" ]; then + keytool -importcert -cacerts -noprompt \ + -storepass changeit \ + -file "/rems/certs/${certfile}" \ + -alias "${certfile}" -certfile=$(ls /rems/certs 2>/dev/null) -parameters=false -cmd_prefix="" -cmd="" -full_cmd="" -declare -a cmd_array - -if [ ! -z ${certfile} ] && [ "${certfile}" != "null" ] ; then - keytool -importcert -cacerts -noprompt \ - -storepass changeit \ - -file /rems/certs/${certfile} \ - -alias ${certfile} - - keytool -storepasswd -cacerts \ - -storepass changeit \ - -new $(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 20) -fi - -if [ "${CMD}" ] ; then - IFS=';' read -r -a cmd_array <<< "${CMD}" -elif [ "${COMMANDS}" ] ; then - IFS=' ' read -r -a cmd_array <<< "${COMMANDS}" -else - cmd_array=("run") + keytool -storepasswd -cacerts \ + -storepass changeit \ + -new "$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 20)" fi -for cmd in "${cmd_array[@]}" -do - [ "${cmd}" = "run" ] && cmd_prefix="exec" +# Dispatch by CMD +case "${CMD:-start}" in + migrate) + echo "########## RUNNING REMS MIGRATION ##########" + exec java -Drems.config=config/config.edn -jar rems.jar migrate + ;; + start) + echo "########## STARTING REMS ##########" + exec java -Drems.config=config/config.edn -jar rems.jar run + ;; + *) + echo "Unknown CMD: '${CMD}' — valid options are: start, migrate" + exit 1 + ;; +esac - FULL_COMMAND="${cmd_prefix} java -Drems.config=config/config.edn -jar rems.jar ${cmd}" - echo "####################" - echo "########## RUNNING COMMAND: ${FULL_COMMAND}" - echo "####################" - ${FULL_COMMAND} -done echo "####################" echo "########## CONTAINER STARTUP FINISHED" From 1ed13b089a079ff9521b745cebef96f55653d3ca Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Sun, 3 Aug 2025 17:42:05 +1000 Subject: [PATCH 64/80] fix middleware order to allow API key-based POST requests without CSRF token --- src/clj/rems/middleware.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clj/rems/middleware.clj b/src/clj/rems/middleware.clj index 893b33046d..03880d2da5 100644 --- a/src/clj/rems/middleware.clj +++ b/src/clj/rems/middleware.clj @@ -310,8 +310,8 @@ wrap-role-headers wrap-context wrap-user - wrap-api-key-or-csrf-token auth/wrap-auth + wrap-api-key-or-csrf-token wrap-entry-logging (wrap-defaults (wrap-defaults-settings)) wrap-cache-control From 8582acd993bcca05aa9914230b910c4e6047e997 Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Sun, 3 Aug 2025 18:11:34 +1000 Subject: [PATCH 65/80] revert crsf order --- src/clj/rems/middleware.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clj/rems/middleware.clj b/src/clj/rems/middleware.clj index 03880d2da5..893b33046d 100644 --- a/src/clj/rems/middleware.clj +++ b/src/clj/rems/middleware.clj @@ -310,8 +310,8 @@ wrap-role-headers wrap-context wrap-user - auth/wrap-auth wrap-api-key-or-csrf-token + auth/wrap-auth wrap-entry-logging (wrap-defaults (wrap-defaults-settings)) wrap-cache-control From 6d5b1d0a33bef26fa6bafd005a511d5203837bbe Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Sun, 3 Aug 2025 19:28:21 +1000 Subject: [PATCH 66/80] revert debugging --- src/clj/rems/auth/auth.clj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clj/rems/auth/auth.clj b/src/clj/rems/auth/auth.clj index ea5ff36cd8..14a1091afc 100644 --- a/src/clj/rems/auth/auth.clj +++ b/src/clj/rems/auth/auth.clj @@ -46,6 +46,7 @@ (:request-method request) (:uri request)) false))))) + (defn wrap-auth [handler] (wrap-uses-valid-api-key From cffb2e96be1ca76a1b30969a7e0a0a9c5d966a64 Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Mon, 4 Aug 2025 21:37:34 +1000 Subject: [PATCH 67/80] add curl to image --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0518d614ac..318f239849 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM eclipse-temurin:17-jre-alpine -RUN apk add --no-cache bash gettext +RUN apk add --no-cache bash gettext curl RUN mkdir -p /rems/keys From 308d4158b41bbb2f276a2aad5200b96da9a9d685 Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Mon, 4 Aug 2025 22:11:29 +1000 Subject: [PATCH 68/80] remove curl --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 318f239849..0518d614ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM eclipse-temurin:17-jre-alpine -RUN apk add --no-cache bash gettext curl +RUN apk add --no-cache bash gettext RUN mkdir -p /rems/keys From c7a30800f124ce1ab6da7e5fa63334d1254f0bdd Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Tue, 19 Aug 2025 18:32:12 +1000 Subject: [PATCH 69/80] logout url --- config.edn.template | 2 + docker-entrypoint.sh | 12 ++ package-lock.json | 449 ++++++++++++++++++++++++++++++++++++++++--- package.json | 5 + 4 files changed, 442 insertions(+), 26 deletions(-) diff --git a/config.edn.template b/config.edn.template index 538aa8a86f..72f6c5fbcb 100644 --- a/config.edn.template +++ b/config.edn.template @@ -12,6 +12,8 @@ :oidc-userid-attributes [{:attribute "sub"}] :oidc-name-attributes ["name" "unique_name" "family_name"] :oidc-email-attributes ["email"] + :oidc-logout-redirect-url "${OIDC_LOGOUT_REDIRECT_URL}" + :oidc-perform-revoke-on-logout true :log-authentication-details true :languages [:en] :catalogue-is-public true diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 331ee35453..c3db963871 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -17,6 +17,18 @@ urlencode() { echo "$encoded" } +# Ensure PUBLIC_URL ends with a trailing slash for post_logout_redirect_uri +PUBLIC_URL="${PUBLIC_URL%/}/" + +# Derive Auth0 base from the configured OIDC metadata URL +# e.g. https://tenant.au.auth0.com/.well-known/openid-configuration -> https://tenant.au.auth0.com +AUTH0_BASE="${OIDC_METADATA_URL%/.well-known/openid-configuration}" +AUTH0_BASE="${AUTH0_BASE%/}" + +# Build the logout URL dynamically (no hard-coding) +ENC_RETURN="$(urlencode "${PUBLIC_URL}")" +export OIDC_LOGOUT_REDIRECT_URL="${AUTH0_BASE}/oidc/logout?client_id=${OIDC_CLIENT_ID}&post_logout_redirect_uri=${ENC_RETURN}" + export DB_PASSWORD_ENCODED=$(urlencode "$DB_PASSWORD") # Write Visa key files diff --git a/package-lock.json b/package-lock.json index cf8a61aa2e..ebce7832ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,7 +4,12 @@ "requires": true, "packages": { "": { + "dependencies": { + "aws-cdk-lib": "^2.211.0", + "constructs": "^10.4.2" + }, "devDependencies": { + "@types/node": "^24.3.0", "diff-match-patch": "1.0.5", "follow-redirects": "1.15.6", "karma": "6.4.3", @@ -31,6 +36,54 @@ "node": ">=6.0.0" } }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.242", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.242.tgz", + "integrity": "sha512-4c1bAy2ISzcdKXYS1k4HYZsNrgiwbiDzj36ybwFVxEWZXVAP0dimQTCaB9fxu7sWzEjw3d+eaw6Fon+QTfTIpQ==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", + "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "48.4.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-48.4.0.tgz", + "integrity": "sha512-pWk5oucfA4Ywt0g5sjr8uABzTBNBrMfxVkHqc7b9jUYlMoY9CzCiOAcCdVLaqrtFp63a+z0M4s1sf6gaIkbeaA==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.2" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.2", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@babel/code-frame": { "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", @@ -607,12 +660,13 @@ } }, "node_modules/@types/node": { - "version": "22.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", - "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~7.10.0" } }, "node_modules/@types/parse-json": { @@ -759,6 +813,356 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-cdk-lib": { + "version": "2.211.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.211.0.tgz", + "integrity": "sha512-wrEPu25572HUJwySzL/qf/fFM+a22X7HYpq1uqcjAn4sVL+h52WjVjnI7rDAuhBp6efX6+Jhmw7jZDMql4/+Cw==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "2.2.242", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^48.2.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.0", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^3.1.2", + "punycode": "^2.3.1", + "semver": "^7.7.2", + "table": "^6.9.0", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.17.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.12", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.0.6", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.7.2", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -777,8 +1181,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -867,7 +1270,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1187,8 +1589,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/connect": { "version": "3.7.0", @@ -1232,6 +1633,12 @@ "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", "dev": true }, + "node_modules/constructs": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", + "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==", + "license": "Apache-2.0" + }, "node_modules/content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", @@ -1810,7 +2217,6 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1965,8 +2371,7 @@ "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, "node_modules/has": { "version": "1.0.3", @@ -2359,7 +2764,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -2556,7 +2960,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -2565,7 +2968,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -2589,7 +2991,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2984,8 +3385,7 @@ "node_modules/punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, "node_modules/qjobs": { "version": "1.2.0", @@ -3296,8 +3696,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "peer": true, "bin": { "semver": "bin/semver.js" } @@ -3781,16 +4179,16 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT" }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true, "engines": { "node": ">= 10.0.0" } @@ -4057,7 +4455,6 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, "engines": { "node": ">= 6" } diff --git a/package.json b/package.json index b2bc3c9595..45b36e0b01 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "shadow-watch": "shadow-cljs watch app" }, "devDependencies": { + "@types/node": "^24.3.0", "diff-match-patch": "1.0.5", "follow-redirects": "1.15.6", "karma": "6.4.3", @@ -17,5 +18,9 @@ "react-dom": "17.0.2", "react-select": "5.8.0", "shadow-cljs": "2.28.3" + }, + "dependencies": { + "aws-cdk-lib": "^2.211.0", + "constructs": "^10.4.2" } } From 7b0444dee54d2344f00c89982f58263dfd1143ae Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Wed, 20 Aug 2025 12:41:19 +1000 Subject: [PATCH 70/80] admin tools image --- .../workflows/admin-utils-docker-build.yml | 51 +++++++++++++++++++ adminUtils.Dockerfile | 24 +++++++++ 2 files changed, 75 insertions(+) create mode 100644 .github/workflows/admin-utils-docker-build.yml create mode 100644 adminUtils.Dockerfile diff --git a/.github/workflows/admin-utils-docker-build.yml b/.github/workflows/admin-utils-docker-build.yml new file mode 100644 index 0000000000..020b393015 --- /dev/null +++ b/.github/workflows/admin-utils-docker-build.yml @@ -0,0 +1,51 @@ +name: Build and Push Admin Utils Image + +on: + push: + branches: + - develop + - master + paths: + - 'adminUtils.Dockerfile' + - '.github/workflows/admin-utils-docker-build.yml' + workflow_dispatch: + +permissions: + id-token: write + contents: read + +jobs: + build-and-push: + runs-on: ubuntu-latest + environment: dev + steps: + - uses: actions/checkout@v3 + + - name: Configure AWS credentials (Dev Account) + uses: aws-actions/configure-aws-credentials@v2 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN_DEV }} + aws-region: ${{ secrets.CDK_REGION_DEV }} + + - name: Log in to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ steps.login-ecr.outputs.registry }}/rems-admin-utils + tags: | + type=ref,event=branch + type=sha + ${{ github.ref == 'refs/heads/master' && 'latest' || '' }} + + - name: Build and push Admin Utils image + uses: docker/build-push-action@v5 + with: + context: . + file: adminUtils.Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/adminUtils.Dockerfile b/adminUtils.Dockerfile new file mode 100644 index 0000000000..51b3780de5 --- /dev/null +++ b/adminUtils.Dockerfile @@ -0,0 +1,24 @@ +# adminUtils.Dockerfile +# Pin to postgres:15 on Debian; if you want fewer surprises, you can use :15-bookworm +FROM postgres:15 + +ENV DEBIAN_FRONTEND=noninteractive + +# Minimal, useful admin tooling; netcat-openbsd provides nc +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + jq \ + vim-tiny \ + less \ + procps \ + netcat-openbsd \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# You'll exec with /bin/bash; ensure it's present (it is on postgres:15, but harmless to add) +# RUN apt-get update && apt-get install -y --no-install-recommends bash && rm -rf /var/lib/apt/lists/* + +# Nothing else needed; ECS Exec will override the command (e.g., /bin/bash) + + + From d733bb353f7ef53dc18dd44d21efd6c6dfe6ce72 Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Wed, 20 Aug 2025 12:41:19 +1000 Subject: [PATCH 71/80] admin tools image --- .github/workflows/admin-utils-docker-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/admin-utils-docker-build.yml b/.github/workflows/admin-utils-docker-build.yml index 020b393015..84c17c5af4 100644 --- a/.github/workflows/admin-utils-docker-build.yml +++ b/.github/workflows/admin-utils-docker-build.yml @@ -6,8 +6,8 @@ on: - develop - master paths: - - 'adminUtils.Dockerfile' - - '.github/workflows/admin-utils-docker-build.yml' + - "adminUtils.Dockerfile" + - ".github/workflows/admin-utils-docker-build.yml" workflow_dispatch: permissions: From 6640db56f9720010033d68aa2614ad90e5709907 Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Mon, 1 Dec 2025 13:51:49 +1100 Subject: [PATCH 72/80] smtp config --- config.edn.template | 1 + 1 file changed, 1 insertion(+) diff --git a/config.edn.template b/config.edn.template index 72f6c5fbcb..dd8a3de124 100644 --- a/config.edn.template +++ b/config.edn.template @@ -26,6 +26,7 @@ :catalogue-tree-show-matching-parents true :enable-autosave true :extra-stylesheets {:root "./" :files ["/theme/styles.css"]} + :smtp {:host "${SMTP_HOST}" :user "${SMTP_USER}" :pass "${SMTP_PASSWORD}" :port 587 :ssl true} ;; :theme-path "/rems/theme/theme.edn" ;; :extra-pages-path "/rems/extra-pages" } \ No newline at end of file From 8538aab6fc7b1db8611343100d6cc6f8c3625b49 Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Mon, 1 Dec 2025 16:40:46 +1100 Subject: [PATCH 73/80] change footer to au biocommons --- resources/translations/en.edn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/translations/en.edn b/resources/translations/en.edn index 24a983bd9e..198d0f9c54 100644 --- a/resources/translations/en.edn +++ b/resources/translations/en.edn @@ -583,7 +583,7 @@ ;; %4 - %:invitation-url% - invitation link :workflow-handler-invitation {:message "Dear %:recipient%,\n\n%:invited-by% has invited you to be a handler of applications of workflow %:workflow%.\n\nYou can view the workflow at %:invitation-url%" :subject "Invitation to handle applications"}} - :footer "CSC – IT Center for Science" + :footer "Australian BioCommons" :form {:accepted-licenses "Accepted terms of use" :actions "Actions" :add-comment "Add comment" From 0c49c37990ce9e486d519001fd31c152e42327bb Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Tue, 2 Dec 2025 13:43:41 +1100 Subject: [PATCH 74/80] extra translations --- Dockerfile | 2 ++ config.edn.template | 1 + theme/extra-translations/en.edn | 1 + 3 files changed, 4 insertions(+) create mode 100644 theme/extra-translations/en.edn diff --git a/Dockerfile b/Dockerfile index 0518d614ac..fd7537d003 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ FROM eclipse-temurin:17-jre-alpine RUN apk add --no-cache bash gettext RUN mkdir -p /rems/keys +RUN mkdir -p /rems/theme/extra-translations WORKDIR /rems @@ -13,6 +14,7 @@ ENTRYPOINT ["bash","./docker-entrypoint.sh"] ADD https://github.com/CSCfi/rems/releases/download/v2.38.1/rems.jar /rems/rems.jar COPY config.edn.template /rems/config/config.edn.template COPY example-theme/extra-styles.css /rems/example-theme/extra-styles.css +COPY theme/extra-translations/ed.edn /rems/theme/extra-translations/en.edn COPY docker-entrypoint.sh /rems/docker-entrypoint.sh RUN chmod 664 /opt/java/openjdk/lib/security/cacerts diff --git a/config.edn.template b/config.edn.template index dd8a3de124..46af264889 100644 --- a/config.edn.template +++ b/config.edn.template @@ -27,6 +27,7 @@ :enable-autosave true :extra-stylesheets {:root "./" :files ["/theme/styles.css"]} :smtp {:host "${SMTP_HOST}" :user "${SMTP_USER}" :pass "${SMTP_PASSWORD}" :port 587 :ssl true} + :mail-from "${SMTP_SENDER}" ;; :theme-path "/rems/theme/theme.edn" ;; :extra-pages-path "/rems/extra-pages" } \ No newline at end of file diff --git a/theme/extra-translations/en.edn b/theme/extra-translations/en.edn new file mode 100644 index 0000000000..588ebcce33 --- /dev/null +++ b/theme/extra-translations/en.edn @@ -0,0 +1 @@ +{:t {:footer "Australian Cardiovascular disease Data Commons" }} \ No newline at end of file From 0458ee8690b737f5270c782ef57166397fa5dde5 Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Tue, 2 Dec 2025 15:31:11 +1100 Subject: [PATCH 75/80] fix dockerfile typo --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index fd7537d003..e207a68b25 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ ENTRYPOINT ["bash","./docker-entrypoint.sh"] ADD https://github.com/CSCfi/rems/releases/download/v2.38.1/rems.jar /rems/rems.jar COPY config.edn.template /rems/config/config.edn.template COPY example-theme/extra-styles.css /rems/example-theme/extra-styles.css -COPY theme/extra-translations/ed.edn /rems/theme/extra-translations/en.edn +COPY theme/extra-translations/en.edn /rems/theme/extra-translations/en.edn COPY docker-entrypoint.sh /rems/docker-entrypoint.sh RUN chmod 664 /opt/java/openjdk/lib/security/cacerts From efc8cb200c5232ec38416324fbbb47caf76b237d Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Tue, 2 Dec 2025 17:57:37 +1100 Subject: [PATCH 76/80] update to tls --- config.edn.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.edn.template b/config.edn.template index 46af264889..430686fcf1 100644 --- a/config.edn.template +++ b/config.edn.template @@ -26,7 +26,7 @@ :catalogue-tree-show-matching-parents true :enable-autosave true :extra-stylesheets {:root "./" :files ["/theme/styles.css"]} - :smtp {:host "${SMTP_HOST}" :user "${SMTP_USER}" :pass "${SMTP_PASSWORD}" :port 587 :ssl true} + :smtp {:host "${SMTP_HOST}" :user "${SMTP_USER}" :pass "${SMTP_PASSWORD}" :port 587 :tls true} :mail-from "${SMTP_SENDER}" ;; :theme-path "/rems/theme/theme.edn" ;; :extra-pages-path "/rems/extra-pages" From 59fb5c499428155c058e4c2403a6104baa0df775 Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Wed, 3 Dec 2025 14:21:46 +1100 Subject: [PATCH 77/80] styles update --- Dockerfile | 3 +- config.edn.template | 4 +-- resources/translations/en.edn | 2 +- theme/extra-translations/en.edn | 6 +++- theme/public/img/login.png | Bin 0 -> 4220 bytes theme/public/img/logo_medium.png | Bin 0 -> 53036 bytes theme/public/img/logo_small.png | Bin 0 -> 53615 bytes theme/style.css | 16 +++++++++ theme/theme.edn | 59 +++++++++++++++++++++++++++++++ 9 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 theme/public/img/login.png create mode 100644 theme/public/img/logo_medium.png create mode 100644 theme/public/img/logo_small.png create mode 100644 theme/style.css create mode 100644 theme/theme.edn diff --git a/Dockerfile b/Dockerfile index e207a68b25..902b5d3b69 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,8 +13,9 @@ ENTRYPOINT ["bash","./docker-entrypoint.sh"] ADD https://github.com/CSCfi/rems/releases/download/v2.38.1/rems.jar /rems/rems.jar COPY config.edn.template /rems/config/config.edn.template -COPY example-theme/extra-styles.css /rems/example-theme/extra-styles.css +COPY example-theme/extra-styles.css /rems/theme/extra-styles.css COPY theme/extra-translations/en.edn /rems/theme/extra-translations/en.edn +COPY theme/theme.edn /rems/theme/theme.edn COPY docker-entrypoint.sh /rems/docker-entrypoint.sh RUN chmod 664 /opt/java/openjdk/lib/security/cacerts diff --git a/config.edn.template b/config.edn.template index 430686fcf1..0ac6c3bb1c 100644 --- a/config.edn.template +++ b/config.edn.template @@ -28,6 +28,6 @@ :extra-stylesheets {:root "./" :files ["/theme/styles.css"]} :smtp {:host "${SMTP_HOST}" :user "${SMTP_USER}" :pass "${SMTP_PASSWORD}" :port 587 :tls true} :mail-from "${SMTP_SENDER}" - ;; :theme-path "/rems/theme/theme.edn" - ;; :extra-pages-path "/rems/extra-pages" + :theme-path "/rems/theme/theme.edn" + :extra-pages-path "/rems/extra-pages" } \ No newline at end of file diff --git a/resources/translations/en.edn b/resources/translations/en.edn index 198d0f9c54..27240b08c4 100644 --- a/resources/translations/en.edn +++ b/resources/translations/en.edn @@ -663,7 +663,7 @@ :unknown "There was a problem logging you in. Please check your identity provider."} :fake-text "" :fake-title "" - :intro [:div [:h1 "Welcome to REMS"] [:p "This is a demo environment for testing the REMS software. The demo environment has a couple of fictional datasets to which you can apply for access. More information is available on " [:a {:href "/extra-pages/about"} "About"] " page."]] + :intro [:div [:h1 "Dataset Permission Management"] [:p "REMS is a service for managing dataset permissions." [:a {:href "/extra-pages/about"} "About"] " page."]] :intro2 "" :login "Login" :oidc-text "" diff --git a/theme/extra-translations/en.edn b/theme/extra-translations/en.edn index 588ebcce33..4cb251221c 100644 --- a/theme/extra-translations/en.edn +++ b/theme/extra-translations/en.edn @@ -1 +1,5 @@ -{:t {:footer "Australian Cardiovascular disease Data Commons" }} \ No newline at end of file +{:t {:footer "Australian BioCommons" + :header {:title "BioCommons REMS"} + :login {:login [:img {:src "img/ls_login.png" :height "50px"}] + :intro [:div [:h5 "Dataset Permission Management"] [:p "REMS is a service for managing dataset permissions."]] + :intro2 "" :text ""}}} \ No newline at end of file diff --git a/theme/public/img/login.png b/theme/public/img/login.png new file mode 100644 index 0000000000000000000000000000000000000000..b8edb683ae32271eb9d3639618b3c52b2359d8d6 GIT binary patch literal 4220 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 Q)8sv1q;IZQiop{82hfbG`~Uy| literal 0 HcmV?d00001 diff --git a/theme/public/img/logo_medium.png b/theme/public/img/logo_medium.png new file mode 100644 index 0000000000000000000000000000000000000000..666a1daeb0886bcd0b106379d9e9a9db94ca7c17 GIT binary patch literal 53036 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 K%n;Rm^nU=|IF4oj literal 0 HcmV?d00001 diff --git a/theme/public/img/logo_small.png b/theme/public/img/logo_small.png new file mode 100644 index 0000000000000000000000000000000000000000..a541c1027732a0fed0e60ca987a1be00e6c2e7c8 GIT binary patch literal 53615 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
i-}ov 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%0DRdkT(M4coZ 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 literal 0 HcmV?d00001 diff --git a/theme/style.css b/theme/style.css new file mode 100644 index 0000000000..0d05957058 --- /dev/null +++ b/theme/style.css @@ -0,0 +1,16 @@ +.logo { + margin-top: 50px; +} + +.login-btn { + max-height: initial !important; + padding: 0; + background-color: #fff; + border-color: #fff; + border-radius: 25px; +} + +.login-btn:hover { + background-color: #fff; + border-color: #fff; +} diff --git a/theme/theme.edn b/theme/theme.edn new file mode 100644 index 0000000000..2e8527f37b --- /dev/null +++ b/theme/theme.edn @@ -0,0 +1,59 @@ +{;; This styling is from the original REMS project + ;; https://github.com/CSCfi/rems/blob/master/example-theme/theme.edn + :font-family "'Lato', sans-serif" + :color1 "#cbd0d5" + :color2 "#008278" + :color3 "#64727e" + :color4 "#830051" + :nav-color "#595959" + :nav-active-color "#830051" + + :alert-success-color "#3c763d" + :alert-success-bgcolor "#dff0d8" + :alert-success-bordercolor "#3c763d" + :text-success "#28a745!important" ; .text and .bg colors need !important to take hold + :bg-success "#28a745!important" + + :alert-danger-color "#a94442" + :alert-danger-bgcolor "#f2dede" + :alert-danger-bordercolor "#a94442" + :text-danger "#dc3545!important" + :bg-danger "#dc3545!important" + + :alert-warning-color "#8a6d3b" + :alert-warning-bgcolor "#fcf8e3" + :alert-warning-bordercolor "#8a6d3b" + :bg-warning "#ffc107!important" + + :alert-info-color "#31708f" + :alert-info-bgcolor "#d9edf7" + :alert-info-bordercolor "#31708f" + :text-info "#17a2b8!important" + :bg-info "#17a2b8!important" + + :img-path "/img/" + :logo-bgcolor nil + :logo-name-en "logo_medium.png" + :logo-name-sm-en "logo_small.png" + :logo-content-origin "initial" + + :navbar-logo-name "logo_small.png" + :navbar-logo-name-en "logo_small.png" + + :phase-color "#111" + :phase-bgcolor "#f8f8f8" + :phase-bgcolor-active "#e8e8e8" + :phase-bgcolor-completed "#dff0d8" + :collapse-bgcolor "#fff" + :collapse-heading-color "#008278" + :table-bgcolor "#f8f8f8" + :table-heading-color "#fff" + :table-heading-bgcolor "#008278" ;"#006778" ;"#006fb1"; "#008278" "#eef2f8" + :table-hover-bgcolor "#b7dddb"; "#eef2f8"; "#c9e2ae" ; "#f2f2f2" + :table-text-color "#111" + :header-border "4px solid #ccc" + :table-shadow "10px -12px 21px -14px rgba(0,0,0,0.75)" + :link-color "#025b96" + :footer-color "#595959" + :footer-bgcolor "#dfe1e3" + :button-navbar-font-weight 400} \ No newline at end of file From c4ab9eefddbe022a7e157fd5c1dfebe5f3dc43cf Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Wed, 3 Dec 2025 14:24:17 +1100 Subject: [PATCH 78/80] update dockerfile --- Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 902b5d3b69..c1924d6b94 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,9 +13,8 @@ ENTRYPOINT ["bash","./docker-entrypoint.sh"] ADD https://github.com/CSCfi/rems/releases/download/v2.38.1/rems.jar /rems/rems.jar COPY config.edn.template /rems/config/config.edn.template -COPY example-theme/extra-styles.css /rems/theme/extra-styles.css -COPY theme/extra-translations/en.edn /rems/theme/extra-translations/en.edn -COPY theme/theme.edn /rems/theme/theme.edn +COPY theme/ /rems/theme + COPY docker-entrypoint.sh /rems/docker-entrypoint.sh RUN chmod 664 /opt/java/openjdk/lib/security/cacerts From c8d25c06d297f3f4ed1c1adc85d70fe06225257d Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Wed, 3 Dec 2025 14:39:41 +1100 Subject: [PATCH 79/80] image name fix --- Dockerfile | 2 +- theme/extra-translations/en.edn | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index c1924d6b94..9d45a0a7bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ WORKDIR /rems ENTRYPOINT ["bash","./docker-entrypoint.sh"] ADD https://github.com/CSCfi/rems/releases/download/v2.38.1/rems.jar /rems/rems.jar -COPY config.edn.template /rems/config/config.edn.template +COPY config.edn.template /rems/config/config.edn COPY theme/ /rems/theme COPY docker-entrypoint.sh /rems/docker-entrypoint.sh diff --git a/theme/extra-translations/en.edn b/theme/extra-translations/en.edn index 4cb251221c..418eb86d86 100644 --- a/theme/extra-translations/en.edn +++ b/theme/extra-translations/en.edn @@ -1,5 +1,5 @@ {:t {:footer "Australian BioCommons" :header {:title "BioCommons REMS"} - :login {:login [:img {:src "img/ls_login.png" :height "50px"}] + :login {:login [:img {:src "img/login.png" :height "50px"}] :intro [:div [:h5 "Dataset Permission Management"] [:p "REMS is a service for managing dataset permissions."]] :intro2 "" :text ""}}} \ No newline at end of file From 6d8066a724413bf75d7a18dc4d96d3078a031953 Mon Sep 17 00:00:00 2001 From: Guerdon Mukama Date: Wed, 3 Dec 2025 14:48:15 +1100 Subject: [PATCH 80/80] image name fix --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9d45a0a7bb..c1924d6b94 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ WORKDIR /rems ENTRYPOINT ["bash","./docker-entrypoint.sh"] ADD https://github.com/CSCfi/rems/releases/download/v2.38.1/rems.jar /rems/rems.jar -COPY config.edn.template /rems/config/config.edn +COPY config.edn.template /rems/config/config.edn.template COPY theme/ /rems/theme COPY docker-entrypoint.sh /rems/docker-entrypoint.sh