trek 2.2.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.slim-lint.yml +0 -3
- data/AGENTS.md +9 -2
- data/CHANGELOG.md +73 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +23 -3
- data/Makefile +11 -9
- data/README.md +1 -1
- data/app/assets/icons/trek/chevron-down.svg +1 -0
- data/app/assets/icons/trek/chevron-up.svg +1 -0
- data/app/assets/icons/trek/external.svg +1 -0
- data/app/assets/icons/trek/theme-auto.svg +1 -0
- data/app/assets/icons/trek/theme-dark.svg +1 -0
- data/app/assets/icons/trek/theme-light.svg +1 -0
- data/app/assets/stylesheets/trek/base.css +13 -0
- data/app/components/trek/button_component/button_component.css +9 -0
- data/app/components/trek/button_component.rb +13 -1
- data/app/components/trek/dashboard/tile_component/tile_component.html.slim +1 -1
- data/app/components/trek/dashboard/tile_component.rb +14 -0
- data/app/components/trek/dialog_component/dialog_component.html.slim +6 -2
- data/app/components/trek/dialog_component/dialog_component.js +62 -2
- data/app/components/trek/dropdown_component/dropdown_component.css +69 -0
- data/app/components/trek/dropdown_component/dropdown_component.html.slim +31 -0
- data/app/components/trek/dropdown_component/dropdown_component.js +114 -0
- data/app/components/trek/dropdown_component.rb +34 -0
- data/app/components/trek/form/group_component/group_component.css +34 -0
- data/app/components/trek/header_component/header_component.css +9 -4
- data/app/components/trek/header_component/header_component.html.slim +17 -10
- data/app/components/trek/header_component/header_component.js +29 -5
- data/app/components/trek/header_component.rb +24 -3
- data/app/components/trek/menu_component/menu_component.css +6 -0
- data/app/components/trek/menu_component.rb +29 -4
- data/app/components/trek/toaster/toast_component.rb +1 -1
- data/app/controllers/concerns/trek/authorization.rb +12 -0
- data/app/controllers/concerns/trek/filters.rb +18 -1
- data/app/helpers/trek/page_path_helper.rb +4 -3
- data/app/helpers/trek/theme_helper.rb +16 -1
- data/app/models/concerns/trek/phonable.rb +15 -3
- data/app/models/concerns/trek/sectionable.rb +5 -0
- data/app/models/concerns/trek/users/validations.rb +15 -6
- data/app/uploaders/audio_uploader.rb +11 -0
- data/app/uploaders/record_image_uploader.rb +3 -0
- data/config/locales/trek.en.yml +3 -2
- data/config/locales/trek.fr.yml +1 -0
- data/docs/.vitepress/config.mjs +20 -1
- data/docs/.vitepress/theme/style.css +36 -8
- data/docs/changelog.md +9 -0
- data/docs/customize.md +18 -2
- data/docs/deploy.md +7 -2
- data/docs/getting-started.md +1 -1
- data/docs/index.md +8 -8
- data/docs/public/apple-touch-icon.png +0 -0
- data/docs/public/favicon.ico +0 -0
- data/docs/public/favicon.png +0 -0
- data/docs/public/trek-mark.webp +0 -0
- data/docs/reference/components.md +15 -0
- data/docs/reference/concerns/phonable.md +27 -1
- data/docs/reference/controllers.md +8 -3
- data/docs/reference/environment-variables.md +2 -0
- data/docs/reference/forms.md +2 -0
- data/docs/reference/generators/install.md +4 -1
- data/docs/reference/icons.md +1 -1
- data/docs/reference/models/tag.md +1 -1
- data/docs/reference/models/user.md +6 -4
- data/docs/reference/uploaders.md +31 -6
- data/docs/what-is-trek.md +1 -1
- data/lib/generators/trek/install/admin/routes_generator.rb +7 -1
- data/lib/generators/trek/install/appsignal_generator.rb +102 -0
- data/lib/generators/trek/install/model_specs_generator.rb +64 -0
- data/lib/generators/trek/install/postmark_generator.rb +3 -0
- data/lib/generators/trek/install/ruby_generator.rb +1 -1
- data/lib/generators/trek/install/scalingo_generator.rb +1 -1
- data/lib/generators/trek/install/uploaders_generator.rb +2 -0
- data/lib/generators/trek/install_generator.rb +5 -0
- data/lib/generators/trek/taxonomies_generator.rb +7 -0
- data/lib/generators/trek/templates/.gitlab-ci.yml +1 -1
- data/lib/generators/trek/templates/config/appsignal.rb.tt +25 -0
- data/lib/generators/trek/templates/controllers/admin/uploads_controller.rb +4 -0
- data/lib/generators/trek/templates/controllers/admin/user_password_resets_controller.rb +7 -0
- data/lib/generators/trek/templates/controllers/admin/users_controller.rb +4 -1
- data/lib/generators/trek/templates/controllers/concerns/appsignal_context.rb +37 -0
- data/lib/generators/trek/templates/db/seeds/setup/fragments.rb +3 -0
- data/lib/generators/trek/templates/db/seeds/setup/users.rb +6 -0
- data/lib/generators/trek/templates/db/seeds/test/cases/page_tree.rb +5 -0
- data/lib/generators/trek/templates/initializers/appsignal_context.rb +5 -0
- data/lib/generators/trek/templates/locales/scaffold/model.en.yml.tt +1 -1
- data/lib/generators/trek/templates/migrations/fragments.rb.erb +1 -1
- data/lib/generators/trek/templates/migrations/menu_nodes.rb.erb +0 -1
- data/lib/generators/trek/templates/migrations/tag_categories.rb.erb +1 -1
- data/lib/generators/trek/templates/migrations/taggings.rb.erb +1 -1
- data/lib/generators/trek/templates/migrations/users.rb.erb +2 -1
- data/lib/generators/trek/templates/models/tag.rb +6 -1
- data/lib/generators/trek/templates/policies/admin/tag_policy.rb +1 -1
- data/lib/generators/trek/templates/policies/external_link_policy.rb +1 -1
- data/lib/generators/trek/templates/spec/fixtures/files/image.png +0 -0
- data/lib/generators/trek/templates/spec/fixtures/files/sound.mp3 +0 -0
- data/lib/generators/trek/templates/spec/models/page_spec.rb +24 -0
- data/lib/generators/trek/templates/spec/models/user_spec.rb +109 -0
- data/lib/generators/trek/templates/spec/policies/admin/external_link_policy_spec.rb +17 -0
- data/lib/generators/trek/templates/spec/policies/admin/fragment_policy_spec.rb +33 -0
- data/lib/generators/trek/templates/spec/policies/admin/page_policy_spec.rb +39 -0
- data/lib/generators/trek/templates/spec/policies/admin/preview_policy_spec.rb +9 -0
- data/lib/generators/trek/templates/spec/policies/admin/tag_category_policy_spec.rb +31 -0
- data/lib/generators/trek/templates/spec/policies/admin/tag_policy_spec.rb +31 -0
- data/lib/generators/trek/templates/spec/policies/admin/user_policy_spec.rb +58 -0
- data/lib/generators/trek/templates/spec/requests/admin/fragments_spec.rb +50 -0
- data/lib/generators/trek/templates/spec/requests/admin/pages_spec.rb +47 -0
- data/lib/generators/trek/templates/spec/requests/admin/uploads_spec.rb +22 -0
- data/lib/generators/trek/templates/spec/requests/admin/user_password_resets_spec.rb +78 -0
- data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +51 -0
- data/lib/generators/trek/templates/spec/requests/admin/users_spec.rb +99 -0
- data/lib/generators/trek/templates/views/admin/user_password_resets/edit.html.slim +1 -1
- data/lib/generators/trek/templates/views/admin/user_password_resets/new.html.slim +1 -1
- data/lib/generators/trek/templates/views/admin/user_sessions/new.html.slim +2 -2
- data/lib/generators/trek/templates/views/admin/users/theme.turbo_stream.slim +2 -0
- data/lib/generators/trek/upgrade/v2_generator.rb +3 -0
- data/lib/tasks/trek/attachments.rake +18 -0
- data/lib/trek/cached_attachments.rb +53 -0
- data/lib/trek/saved_attachments_reload.rb +44 -0
- data/lib/trek/version.rb +1 -1
- data/lib/trek.rb +4 -0
- data/package.json +1 -1
- data/postcss.config.js +36 -0
- data/trek.gemspec +6 -0
- metadata +89 -3
- data/app/assets/icons/trek/theme.svg +0 -11
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe Admin::FragmentPolicy do
|
|
4
|
+
def policy(role, record = Fragment)
|
|
5
|
+
described_class.new(record, user: users.new(role:))
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
%w[admin editor].each do |role|
|
|
9
|
+
it "lets #{role}s list, create, edit and delete fragments" do
|
|
10
|
+
expect(%i[index? create? update? destroy?].map { |rule| policy(role).apply(rule) }).to all(be(true))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
%w[user reader guest].each do |role|
|
|
15
|
+
it "keeps #{role}s away from fragments" do
|
|
16
|
+
expect(%i[index? create? update? destroy?].map { |rule| policy(role).apply(rule) }).to all(be(false))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "lets privileged users edit a fragment" do
|
|
21
|
+
expect(policy("editor").permitted_attributes).to contain_exactly(:content, :key, :namespace, :title)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "lets other roles edit nothing" do
|
|
25
|
+
expect(policy("reader").permitted_attributes).to be_empty
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "shows other roles no fragment" do
|
|
29
|
+
Fragment.create!(key: "footer", namespace: "layout", title: "Footer")
|
|
30
|
+
|
|
31
|
+
expect(policy("reader").apply_scope(Fragment.all, type: :active_record_relation)).to be_empty
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe Admin::PagePolicy do
|
|
4
|
+
def policy(role, record = Page)
|
|
5
|
+
described_class.new(record, user: users.new(role:))
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
%w[admin editor].each do |role|
|
|
9
|
+
it "lets #{role}s list, create, edit and delete pages" do
|
|
10
|
+
expect(%i[index? create? update? destroy?].map { |rule| policy(role).apply(rule) }).to all(be(true))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
%w[user reader guest].each do |role|
|
|
15
|
+
it "keeps #{role}s away from pages" do
|
|
16
|
+
expect(%i[index? create? update? destroy?].map { |rule| policy(role).apply(rule) }).to all(be(false))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "lets privileged users edit a page's content" do
|
|
21
|
+
expect(policy("editor").permitted_attributes).to include(:title, :slug, :description, :content)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "lets other roles edit nothing" do
|
|
25
|
+
expect(policy("reader").permitted_attributes).to be_empty
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "shows privileged users every page" do
|
|
29
|
+
seed "cases/page_tree"
|
|
30
|
+
|
|
31
|
+
expect(policy("editor").apply_scope(Page.all, type: :active_record_relation)).to match_array(Page.all)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "shows other roles no page" do
|
|
35
|
+
seed "cases/page_tree"
|
|
36
|
+
|
|
37
|
+
expect(policy("reader").apply_scope(Page.all, type: :active_record_relation)).to be_empty
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe Admin::TagCategoryPolicy do
|
|
4
|
+
let(:category) { TagCategory.create!(name: "Topics", key: "topics") }
|
|
5
|
+
|
|
6
|
+
def policy(role, record = TagCategory)
|
|
7
|
+
described_class.new(record, user: users.new(role:))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
%w[admin editor].each do |role|
|
|
11
|
+
it "lets #{role}s list, create and edit tag categories" do
|
|
12
|
+
expect(%i[index? create? update?].map { |rule| policy(role).apply(rule) }).to all(be(true))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
%w[user reader guest].each do |role|
|
|
17
|
+
it "keeps #{role}s away from tag categories" do
|
|
18
|
+
expect(%i[index? create? update?].map { |rule| policy(role).apply(rule) }).to all(be(false))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "lets privileged users delete an empty category" do
|
|
23
|
+
expect(policy("editor", category).apply(:destroy?)).to be(true)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "keeps a category with tags from being deleted" do
|
|
27
|
+
Tag.create!(name: "News", key: "news", category:)
|
|
28
|
+
|
|
29
|
+
expect(policy("admin", category.reload).apply(:destroy?)).to be(false)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe Admin::TagPolicy do
|
|
4
|
+
let(:tag) { Tag.create!(name: "News", key: "news") }
|
|
5
|
+
|
|
6
|
+
def policy(role, record = Tag)
|
|
7
|
+
described_class.new(record, user: users.new(role:))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
%w[admin editor].each do |role|
|
|
11
|
+
it "lets #{role}s list, create and edit tags" do
|
|
12
|
+
expect(%i[index? create? update?].map { |rule| policy(role).apply(rule) }).to all(be(true))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
%w[user reader guest].each do |role|
|
|
17
|
+
it "keeps #{role}s away from tags" do
|
|
18
|
+
expect(%i[index? create? update?].map { |rule| policy(role).apply(rule) }).to all(be(false))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "lets privileged users delete an unused tag" do
|
|
23
|
+
expect(policy("editor", tag).apply(:destroy?)).to be(true)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "keeps a tag in use from being deleted" do
|
|
27
|
+
allow(tag).to receive(:taggings).and_return([Tagging.new])
|
|
28
|
+
|
|
29
|
+
expect(policy("admin", tag).apply(:destroy?)).to be(false)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe Admin::UserPolicy do
|
|
4
|
+
let(:admin) { users.create(role: :admin) }
|
|
5
|
+
let(:editor) { users.create(role: :editor) }
|
|
6
|
+
|
|
7
|
+
def policy(user, record = User)
|
|
8
|
+
described_class.new(record, user:)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "lets admins list, create and delete users" do
|
|
12
|
+
expect(%i[index? create? destroy?].map { |rule| policy(admin).apply(rule) }).to all(be(true))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "lets admins edit any user" do
|
|
16
|
+
expect(policy(admin, editor).apply(:update?)).to be(true)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "keeps editors away from the users list" do
|
|
20
|
+
expect(%i[index? create? destroy?].map { |rule| policy(editor).apply(rule) }).to all(be(false))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "lets editors see and edit themselves" do
|
|
24
|
+
expect(%i[show? update?].map { |rule| policy(editor, editor).apply(rule) }).to all(be(true))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "keeps editors away from other users" do
|
|
28
|
+
expect(%i[show? update?].map { |rule| policy(editor, admin).apply(rule) }).to all(be(false))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "lets admins change a user's role" do
|
|
32
|
+
expect(policy(admin, editor).permitted_attributes).to include(:role)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "lets admins invite the users they create" do
|
|
36
|
+
expect(policy(admin, User).permitted_attributes).to include(:send_invite)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "lets editors change their own details but not their role" do
|
|
40
|
+
expect(policy(editor, editor).permitted_attributes).to contain_exactly(:name, :email, :locale, :theme)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "lets editors change nothing on another user" do
|
|
44
|
+
expect(policy(editor, admin).permitted_attributes).to be_empty
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "shows admins the privileged users only" do
|
|
48
|
+
reader = users.create(role: :reader)
|
|
49
|
+
|
|
50
|
+
expect(described_class.new(user: admin).apply_scope(User.all, type: :active_record_relation)).not_to include(reader)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "shows editors no user" do
|
|
54
|
+
admin
|
|
55
|
+
|
|
56
|
+
expect(described_class.new(user: editor).apply_scope(User.all, type: :active_record_relation)).to be_empty
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe "Admin fragments", type: :request do
|
|
4
|
+
let!(:footer) { fragments.create(key: "footer", namespace: "layout", title: "Footer") }
|
|
5
|
+
|
|
6
|
+
before do
|
|
7
|
+
editor = users.create(role: :editor)
|
|
8
|
+
post admin_user_session_path, params: { email: editor.email, password: users.password }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "lists the fragments" do
|
|
12
|
+
get admin_fragments_path
|
|
13
|
+
|
|
14
|
+
expect(response.body).to include("footer")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "renders the new fragment form" do
|
|
18
|
+
get new_admin_fragment_path
|
|
19
|
+
|
|
20
|
+
expect(response).to have_http_status(:ok)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "creates a fragment" do
|
|
24
|
+
params = { fragment: { key: "tagline", namespace: "home", title: "Tagline" } }
|
|
25
|
+
|
|
26
|
+
expect { post admin_fragments_path, params: }.to change(Fragment, :count).by(1)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "refuses a key already used in the namespace" do
|
|
30
|
+
post admin_fragments_path, params: { fragment: { key: "footer", namespace: "layout", title: "Another footer" } }
|
|
31
|
+
|
|
32
|
+
expect(response).to have_http_status(:unprocessable_entity)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "renders the edit form" do
|
|
36
|
+
get edit_admin_fragment_path(footer)
|
|
37
|
+
|
|
38
|
+
expect(response).to have_http_status(:ok)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "updates a fragment" do
|
|
42
|
+
patch admin_fragment_path(footer), params: { fragment: { title: "Site footer" } }
|
|
43
|
+
|
|
44
|
+
expect(footer.reload.title).to eq("Site footer")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "deletes a fragment" do
|
|
48
|
+
expect { delete admin_fragment_path(footer) }.to change(Fragment, :count).by(-1)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe "Admin pages", type: :request do
|
|
4
|
+
before do
|
|
5
|
+
seed "cases/page_tree"
|
|
6
|
+
editor = users.create(role: :editor)
|
|
7
|
+
post admin_user_session_path, params: { email: editor.email, password: users.password }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it "lists the pages" do
|
|
11
|
+
get admin_pages_path
|
|
12
|
+
|
|
13
|
+
expect(response.body).to include("Company").and include("Our team")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "renders the new page form" do
|
|
17
|
+
get new_admin_page_path
|
|
18
|
+
|
|
19
|
+
expect(response).to have_http_status(:ok)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "creates a page" do
|
|
23
|
+
expect { post admin_pages_path, params: { page: { title: "Contact" } } }.to change(Page, :count).by(1)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "refuses a page without a title" do
|
|
27
|
+
post admin_pages_path, params: { page: { title: "" } }
|
|
28
|
+
|
|
29
|
+
expect(response).to have_http_status(:unprocessable_entity)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "renders the edit form" do
|
|
33
|
+
get edit_admin_page_path(pages.team)
|
|
34
|
+
|
|
35
|
+
expect(response).to have_http_status(:ok)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "updates a page" do
|
|
39
|
+
patch admin_page_path(pages.team), params: { page: { title: "The team" } }
|
|
40
|
+
|
|
41
|
+
expect(pages.team.title).to eq("The team")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "deletes a page" do
|
|
45
|
+
expect { delete admin_page_path(pages.team) }.to change(Page, :count).by(-1)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
# The endpoints the admin's image and sound fields upload to (`Trek::Form::ImageFieldComponent`,
|
|
4
|
+
# `Trek::Form::SoundFieldComponent`).
|
|
5
|
+
RSpec.describe "Admin uploads", type: :request do
|
|
6
|
+
before do
|
|
7
|
+
editor = users.create(role: :editor)
|
|
8
|
+
post admin_user_session_path, params: { email: editor.email, password: users.password }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "stores an uploaded image in the cache and returns its data" do
|
|
12
|
+
post "/admin/upload/image", params: { file: fixture_file_upload("image.png", "image/png") }
|
|
13
|
+
|
|
14
|
+
expect(response.parsed_body["data"]).to include("id", "storage" => "cache")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "stores an uploaded sound in the cache and returns its data" do
|
|
18
|
+
post "/admin/upload/audio", params: { file: fixture_file_upload("sound.mp3", "audio/mpeg") }
|
|
19
|
+
|
|
20
|
+
expect(response.parsed_body["data"]).to include("id", "storage" => "cache")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe "Admin password resets", type: :request do
|
|
4
|
+
let(:editor) { users.create(role: :editor) }
|
|
5
|
+
|
|
6
|
+
it "renders the reset request form" do
|
|
7
|
+
get new_admin_user_password_reset_path
|
|
8
|
+
|
|
9
|
+
expect(response).to have_http_status(:ok)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "emails a reset link to a privileged user" do
|
|
13
|
+
expect { post admin_user_password_resets_path, params: { email: editor.email } }
|
|
14
|
+
.to have_enqueued_mail(Admin::PasswordsMailer, :reset)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "sends nothing for an unknown email" do
|
|
18
|
+
expect { post admin_user_password_resets_path, params: { email: "nobody@example.com" } }
|
|
19
|
+
.not_to have_enqueued_mail
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "answers an unknown email as a known one, so it can't be used to find accounts" do
|
|
23
|
+
post admin_user_password_resets_path, params: { email: "nobody@example.com" }
|
|
24
|
+
|
|
25
|
+
expect(response).to redirect_to(new_admin_user_session_path)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "renders the new password form from a reset link" do
|
|
29
|
+
get edit_admin_user_password_reset_path(editor.password_reset_token)
|
|
30
|
+
|
|
31
|
+
expect(response).to have_http_status(:ok)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "renders the new password form from an invitation link" do
|
|
35
|
+
get edit_admin_user_password_reset_path(editor.generate_token_for(:invite))
|
|
36
|
+
|
|
37
|
+
expect(response).to have_http_status(:ok)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "sends an invalid link back to the reset request form" do
|
|
41
|
+
get edit_admin_user_password_reset_path("not-a-token")
|
|
42
|
+
|
|
43
|
+
expect(response).to redirect_to(new_admin_user_password_reset_path)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "sets the new password" do
|
|
47
|
+
patch admin_user_password_reset_path(editor.password_reset_token), params: { password: "a brand new password" }
|
|
48
|
+
|
|
49
|
+
expect(editor.reload.authenticate("a brand new password")).to be_truthy
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "signs the user out everywhere" do
|
|
53
|
+
post admin_user_session_path, params: { email: editor.email, password: users.password }
|
|
54
|
+
patch admin_user_password_reset_path(editor.password_reset_token), params: { password: "a brand new password" }
|
|
55
|
+
|
|
56
|
+
expect(editor.user_sessions).to be_empty
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "sends an empty password back to the form rather than pretending to change it" do
|
|
60
|
+
token = editor.password_reset_token
|
|
61
|
+
patch admin_user_password_reset_path(token), params: { password: "" }
|
|
62
|
+
|
|
63
|
+
expect(response).to redirect_to(edit_admin_user_password_reset_path(token))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "sends a password shorter than 8 characters back to the form" do
|
|
67
|
+
token = editor.password_reset_token
|
|
68
|
+
patch admin_user_password_reset_path(token), params: { password: "short" }
|
|
69
|
+
|
|
70
|
+
expect(response).to redirect_to(edit_admin_user_password_reset_path(token))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "keeps the old password when the new one is too short" do
|
|
74
|
+
patch admin_user_password_reset_path(editor.password_reset_token), params: { password: "short" }
|
|
75
|
+
|
|
76
|
+
expect(editor.reload.authenticate(users.password)).to be_truthy
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe "Admin sign-in", type: :request do
|
|
4
|
+
let(:editor) { users.create(role: :editor) }
|
|
5
|
+
|
|
6
|
+
it "renders the sign-in form" do
|
|
7
|
+
get new_admin_user_session_path
|
|
8
|
+
|
|
9
|
+
expect(response).to have_http_status(:ok)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "signs a privileged user in and opens the dashboard" do
|
|
13
|
+
post admin_user_session_path, params: { email: editor.email, password: users.password }
|
|
14
|
+
follow_redirect!
|
|
15
|
+
|
|
16
|
+
expect(response).to have_http_status(:ok)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "signs in whatever the case of the email" do
|
|
20
|
+
post admin_user_session_path, params: { email: editor.email.upcase, password: users.password }
|
|
21
|
+
|
|
22
|
+
expect(response).to redirect_to(admin_root_url)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "refuses a wrong password" do
|
|
26
|
+
post admin_user_session_path, params: { email: editor.email, password: "not the password" }
|
|
27
|
+
|
|
28
|
+
expect(response).to redirect_to(new_admin_user_session_path)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "refuses a user who is not privileged" do
|
|
32
|
+
reader = users.create(role: :reader)
|
|
33
|
+
post admin_user_session_path, params: { email: reader.email, password: users.password }
|
|
34
|
+
|
|
35
|
+
expect(response).to redirect_to(new_admin_user_session_path)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "sends a signed-out visitor to the sign-in form" do
|
|
39
|
+
get admin_root_path
|
|
40
|
+
|
|
41
|
+
expect(response).to redirect_to(new_admin_user_session_path)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "signs out" do
|
|
45
|
+
post admin_user_session_path, params: { email: editor.email, password: users.password }
|
|
46
|
+
delete admin_user_session_path
|
|
47
|
+
get admin_root_path
|
|
48
|
+
|
|
49
|
+
expect(response).to redirect_to(new_admin_user_session_path)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe "Admin users", type: :request do
|
|
4
|
+
let(:admin) { users.create(role: :admin, name: "Ada") }
|
|
5
|
+
let(:editor) { users.create(role: :editor, name: "Grace") }
|
|
6
|
+
|
|
7
|
+
def sign_in(user)
|
|
8
|
+
post admin_user_session_path, params: { email: user.email, password: users.password }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
context "when signed in as an admin" do
|
|
12
|
+
before { sign_in(admin) }
|
|
13
|
+
|
|
14
|
+
it "lists the privileged users" do
|
|
15
|
+
editor
|
|
16
|
+
get admin_users_path
|
|
17
|
+
|
|
18
|
+
expect(response.body).to include("Grace")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "leaves the other users out" do
|
|
22
|
+
users.create(role: :reader, name: "Linus")
|
|
23
|
+
get admin_users_path
|
|
24
|
+
|
|
25
|
+
expect(response.body).not_to include("Linus")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "renders the new user form" do
|
|
29
|
+
get new_admin_user_path
|
|
30
|
+
|
|
31
|
+
expect(response).to have_http_status(:ok)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "creates a user and sends them an invitation" do
|
|
35
|
+
params = { user: { email: "new@example.com", role: "editor", send_invite: "1" } }
|
|
36
|
+
|
|
37
|
+
expect { post admin_users_path, params: }
|
|
38
|
+
.to change(User, :count).by(1).and have_enqueued_mail(AdminMailer, :invite)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "changes a user's role" do
|
|
42
|
+
patch admin_user_path(editor), params: { user: { role: "admin" } }
|
|
43
|
+
|
|
44
|
+
expect(editor.reload.role).to eq("admin")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "deletes a user" do
|
|
48
|
+
editor
|
|
49
|
+
|
|
50
|
+
expect { delete admin_user_path(editor) }.to change(User, :count).by(-1)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
context "when signed in as an editor" do
|
|
55
|
+
before { sign_in(editor) }
|
|
56
|
+
|
|
57
|
+
it "is sent back to the dashboard from the users list" do
|
|
58
|
+
get admin_users_path
|
|
59
|
+
|
|
60
|
+
expect(response).to redirect_to(admin_root_path)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "is sent back to the dashboard from another user's profile" do
|
|
64
|
+
get admin_user_path(admin)
|
|
65
|
+
|
|
66
|
+
expect(response).to redirect_to(admin_root_path)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "is told why they were sent back" do
|
|
70
|
+
get admin_users_path
|
|
71
|
+
|
|
72
|
+
expect(flash[:alert]).to eq(I18n.t!("notices.unauthorized"))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "gets a 403 for a JSON request they aren't allowed" do
|
|
76
|
+
get admin_users_path(format: :json)
|
|
77
|
+
|
|
78
|
+
expect(response).to have_http_status(:forbidden)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "edits their own profile" do
|
|
82
|
+
patch admin_user_path(editor), params: { user: { name: "Grace H." } }
|
|
83
|
+
|
|
84
|
+
expect(editor.reload.name).to eq("Grace H.")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "cannot change their own role" do
|
|
88
|
+
patch admin_user_path(editor), params: { user: { role: "admin" } }
|
|
89
|
+
|
|
90
|
+
expect(editor.reload.role).to eq("editor")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "switches their admin theme" do
|
|
94
|
+
patch theme_admin_users_path(format: :turbo_stream), params: { theme: "dark" }
|
|
95
|
+
|
|
96
|
+
expect(editor.reload.theme).to eq("dark")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
= render Trek::GateComponent.new
|
|
2
2
|
= form_with url: admin_user_session_path do |f|
|
|
3
|
-
= f.group :email
|
|
3
|
+
= f.group :email, model_name: User.model_name
|
|
4
4
|
= f.email_field :email,
|
|
5
5
|
required: true,
|
|
6
6
|
autofocus: @email.nil?,
|
|
7
7
|
autocomplete: "username",
|
|
8
8
|
value: @email
|
|
9
9
|
|
|
10
|
-
= f.group :password
|
|
10
|
+
= f.group :password, model_name: User.model_name
|
|
11
11
|
= f.password_field :password,
|
|
12
12
|
required: true,
|
|
13
13
|
autofocus: @email.present?,
|
|
@@ -60,6 +60,9 @@ module Trek
|
|
|
60
60
|
content.sub!(/^#{key}:\n/, "") if YAML.safe_load(content)&.fetch(key, :absent).nil?
|
|
61
61
|
end
|
|
62
62
|
content.gsub!(/\n{3,}/, "\n\n")
|
|
63
|
+
# No blank line left at the end where a removed block was: `preapprove_npm_package` appends
|
|
64
|
+
# its own, and a second run would otherwise collapse the double blank line it makes.
|
|
65
|
+
content.sub!(/\n+\z/, "\n")
|
|
63
66
|
|
|
64
67
|
create_file ".yarnrc.yml", content, force: true
|
|
65
68
|
rescue Psych::Exception
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
namespace :trek do
|
|
2
|
+
namespace :attachments do
|
|
3
|
+
desc "Promote Shrine attachments left in the cache storage (DRY_RUN=1 only lists them)"
|
|
4
|
+
task promote_cached: :environment do
|
|
5
|
+
# Loads every model, so that all attachments are found
|
|
6
|
+
Rails.application.eager_load!
|
|
7
|
+
|
|
8
|
+
statuses = Hash.new(0)
|
|
9
|
+
|
|
10
|
+
Trek::CachedAttachments.new.promote(dry_run: ENV["DRY_RUN"].present?) do |entry|
|
|
11
|
+
statuses[entry.status] += 1
|
|
12
|
+
puts [entry.status, "#{entry.record.class.name}##{entry.record.id}.#{entry.name}", entry.file.id].join("\t")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
puts statuses.empty? ? "No attachment left in the cache storage." : statuses.map { "#{_2} #{_1}" }.join(", ")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Trek
|
|
2
|
+
# Finds the records whose Shrine attachments still point to the temporary cache storage —
|
|
3
|
+
# files whose promotion was skipped — and promotes those whose cached file still exists.
|
|
4
|
+
class CachedAttachments
|
|
5
|
+
Entry = Struct.new(:record, :name, :file, :status, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
def self.default_models
|
|
8
|
+
ActiveRecord::Base.descendants.select do |model|
|
|
9
|
+
!model.abstract_class? && model == model.base_class && model.table_exists?
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(models = self.class.default_models)
|
|
14
|
+
@models = models
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Yields an Entry per cached attachment, whose status is :promoted, :promotable (dry run),
|
|
18
|
+
# :missing (the cached file is gone) or :changed (the record changed during promotion).
|
|
19
|
+
def promote(dry_run: false)
|
|
20
|
+
return enum_for(:promote, dry_run:) unless block_given?
|
|
21
|
+
|
|
22
|
+
each_cached_attachment do |record, name|
|
|
23
|
+
attacher = record.public_send(:"#{name}_attacher")
|
|
24
|
+
|
|
25
|
+
yield Entry.new(record:, name:, file: attacher.file, status: promote_attacher(attacher, dry_run:))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def promote_attacher(attacher, dry_run:)
|
|
32
|
+
return :missing unless attacher.file.exists?
|
|
33
|
+
return :promotable if dry_run
|
|
34
|
+
|
|
35
|
+
attacher.atomic_promote
|
|
36
|
+
:promoted
|
|
37
|
+
rescue Shrine::AttachmentChanged
|
|
38
|
+
:changed
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def each_cached_attachment(&)
|
|
42
|
+
@models.each do |model|
|
|
43
|
+
model.ancestors.grep(Shrine::Attachment).each do |attachment|
|
|
44
|
+
name = attachment.attachment_name
|
|
45
|
+
column = model.connection.quote_column_name(:"#{name}_data")
|
|
46
|
+
cache_key = attachment.options.fetch(:cache, :cache).to_s
|
|
47
|
+
|
|
48
|
+
model.where("#{column}::jsonb ->> 'storage' = ?", cache_key).find_each { |record| yield record, name }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|