trek 2.5.0 → 2.6.0
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/.node-version +1 -1
- data/AGENTS.md +7 -4
- data/CHANGELOG.md +37 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +16 -3
- data/Makefile +2 -0
- data/app/components/trek/dashboard/tile_component/tile_component.css +9 -0
- data/app/components/trek/dashboard/tile_component/tile_component.html.slim +4 -2
- data/app/components/trek/dashboard/tile_component.rb +12 -2
- data/app/components/trek/form/content_editor_component/content_editor_component.css +5 -0
- data/app/components/trek/form/content_editor_component/content_editor_component.html.slim +19 -93
- data/app/components/trek/form/content_editor_component/content_editor_component.js +94 -251
- data/app/components/trek/form/content_editor_component/extensions/block_view.js +40 -0
- data/app/components/trek/form/content_editor_component/extensions/image_block.js +28 -26
- data/app/components/trek/form/content_editor_component/extensions/prompts_block.js +15 -26
- data/app/components/trek/form/content_editor_component/extensions/table_of_contents.js +1 -3
- data/app/components/trek/form/content_editor_component.rb +15 -0
- data/app/components/trek/form/image_field_component/image_field_component.css +40 -1
- data/app/components/trek/form/image_field_component/image_field_component.html.slim +0 -9
- data/app/components/trek/form/image_field_component.rb +36 -9
- data/app/content/trek/content/cleaner.rb +53 -0
- data/app/content/trek/content/document.rb +2 -2
- data/app/content/trek/content/prompts_block.rb +1 -2
- data/app/content/trek/content.rb +6 -0
- data/app/controllers/concerns/trek/reorderable.rb +24 -6
- data/app/controllers/trek/csp_reports_controller.rb +14 -1
- data/app/controllers/trek/panels/prompts_controller.rb +1 -1
- data/app/controllers/trek/resource_controller.rb +5 -5
- data/app/jobs/trek/purge_expired_sessions_job.rb +4 -3
- data/app/models/concerns/trek/contentable.rb +15 -1
- data/app/models/concerns/trek/users/sign_in_code.rb +70 -0
- data/app/models/concerns/trek/users/sign_in_lockout.rb +4 -2
- data/app/views/trek/panels/links/_form.html.slim +2 -4
- data/app/views/trek/panels/prompts/create.turbo_stream.slim +0 -1
- data/config/locales/trek.en.yml +26 -11
- data/config/locales/trek.fr.yml +26 -11
- data/docs/.vitepress/config.mjs +4 -1
- data/docs/background-jobs.md +69 -0
- data/docs/content-editor.md +219 -0
- data/docs/customize.md +2 -0
- data/docs/deploy.md +2 -2
- data/docs/getting-started.md +1 -1
- data/docs/index.md +1 -1
- data/docs/reference/components.md +2 -2
- data/docs/reference/concerns/contentable.md +10 -4
- data/docs/reference/concerns/formattable.md +1 -1
- data/docs/reference/environment-variables.md +1 -0
- data/docs/reference/formatters.md +2 -30
- data/docs/reference/forms.md +1 -1
- data/docs/reference/generators/audit-log.md +2 -2
- data/docs/reference/generators/install.md +3 -2
- data/docs/reference/generators/public-accounts.md +141 -0
- data/docs/reference/generators/security-headers.md +1 -1
- data/docs/reference/index.md +2 -1
- data/docs/reference/models/user.md +3 -1
- data/docs/what-is-trek.md +1 -1
- data/lib/generators/trek/audit_log_generator.rb +54 -0
- data/lib/generators/trek/install/admin/users_generator.rb +6 -1
- data/lib/generators/trek/install/audit_log_generator.rb +9 -45
- data/lib/generators/trek/install/good_job_generator.rb +161 -0
- data/lib/generators/trek/install/model_specs_generator.rb +3 -3
- data/lib/generators/trek/install/ruby_generator.rb +1 -1
- data/lib/generators/trek/install/scalingo_generator.rb +7 -45
- data/lib/generators/trek/install_generator.rb +15 -0
- data/lib/generators/trek/public_accounts_generator.rb +267 -0
- data/lib/generators/trek/scalingo_generator.rb +55 -0
- data/lib/generators/trek/templates/.gitlab-ci.yml +1 -1
- data/lib/generators/trek/templates/controllers/admin/password_renewals_controller.rb +11 -0
- data/lib/generators/trek/templates/controllers/admin/user_sessions_controller.rb +1 -1
- data/lib/generators/trek/templates/controllers/admin/users_controller.rb +0 -8
- data/lib/generators/trek/templates/initializers/content_security_policy.rb +3 -2
- data/lib/generators/trek/templates/initializers/good_job.rb.tt +38 -0
- data/lib/generators/trek/templates/jobs/mail_delivery_job.rb +6 -0
- data/lib/generators/trek/templates/layouts/admin.html.slim +5 -0
- data/lib/generators/trek/templates/mailers/admin_mailer.rb +4 -2
- data/lib/generators/trek/templates/package.json.tt +1 -1
- data/lib/generators/trek/templates/public_accounts/api/authentication.rb.tt +76 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/accounts_controller.rb.tt +22 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/active_sessions_controller.rb.tt +21 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/base_controller.rb.tt +71 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/email_changes_controller.rb.tt +55 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/email_confirmations_controller.rb.tt +19 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/password_resets_controller.rb.tt +28 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/passwords_controller.rb.tt +17 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/registrations_controller.rb.tt +17 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/sessions_controller.rb.tt +45 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/sign_in_codes_controller.rb.tt +15 -0
- data/lib/generators/trek/templates/public_accounts/jobs/sign_in_code_job.rb.tt +13 -0
- data/lib/generators/trek/templates/public_accounts/locales/accounts.en.yml.tt +205 -0
- data/lib/generators/trek/templates/public_accounts/locales/accounts.fr.yml.tt +205 -0
- data/lib/generators/trek/templates/public_accounts/mailers/account_mailer.rb.tt +85 -0
- data/lib/generators/trek/templates/public_accounts/migrations/create_accounts.rb.tt +46 -0
- data/lib/generators/trek/templates/public_accounts/models/account/session.rb.tt +47 -0
- data/lib/generators/trek/templates/public_accounts/models/account/sign_in_code.rb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/models/account.rb.tt +73 -0
- data/lib/generators/trek/templates/public_accounts/spec/api/accounts_spec.rb.tt +90 -0
- data/lib/generators/trek/templates/public_accounts/spec/api/sign_ins_spec.rb.tt +158 -0
- data/lib/generators/trek/templates/public_accounts/spec/models/account/sign_in_code_spec.rb.tt +56 -0
- data/lib/generators/trek/templates/public_accounts/spec/models/account_spec.rb.tt +109 -0
- data/lib/generators/trek/templates/public_accounts/spec/web/accounts_spec.rb.tt +126 -0
- data/lib/generators/trek/templates/public_accounts/spec/web/authentication_spec.rb.tt +43 -0
- data/lib/generators/trek/templates/public_accounts/spec/web/sign_ins_spec.rb.tt +215 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.en.html.slim.tt +10 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.en.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.fr.html.slim.tt +13 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.fr.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.en.html.slim.tt +9 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.en.text.erb.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.fr.html.slim.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.fr.text.erb.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.en.html.slim.tt +10 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.en.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.fr.html.slim.tt +10 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.fr.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.en.html.slim.tt +10 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.en.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.fr.html.slim.tt +13 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.fr.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.en.html.slim.tt +9 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.en.text.erb.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.fr.html.slim.tt +9 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.fr.text.erb.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.en.html.slim.tt +23 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.en.text.erb.tt +14 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.fr.html.slim.tt +24 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.fr.text.erb.tt +14 -0
- data/lib/generators/trek/templates/public_accounts/web/authentication.rb.tt +97 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/accounts_controller.rb.tt +33 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/active_sessions_controller.rb.tt +15 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/base_controller.rb.tt +52 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/email_changes_controller.rb.tt +85 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/email_confirmations_controller.rb.tt +19 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/password_resets_controller.rb.tt +41 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/passwords_controller.rb.tt +23 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/registrations_controller.rb.tt +24 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/sessions_controller.rb.tt +78 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/sign_in_codes_controller.rb.tt +27 -0
- data/lib/generators/trek/templates/public_accounts/web/views/_flash.html.slim.tt +5 -0
- data/lib/generators/trek/templates/public_accounts/web/views/accounts/show.html.slim.tt +52 -0
- data/lib/generators/trek/templates/public_accounts/web/views/email_changes/edit.html.slim.tt +17 -0
- data/lib/generators/trek/templates/public_accounts/web/views/email_changes/new.html.slim.tt +15 -0
- data/lib/generators/trek/templates/public_accounts/web/views/password_resets/edit.html.slim.tt +26 -0
- data/lib/generators/trek/templates/public_accounts/web/views/password_resets/new.html.slim.tt +15 -0
- data/lib/generators/trek/templates/public_accounts/web/views/passwords/edit.html.slim.tt +25 -0
- data/lib/generators/trek/templates/public_accounts/web/views/registrations/new.html.slim.tt +28 -0
- data/lib/generators/trek/templates/public_accounts/web/views/sessions/new.html.slim.tt +38 -0
- data/lib/generators/trek/templates/public_accounts/web/views/sign_in_codes/new.html.slim.tt +17 -0
- data/lib/generators/trek/templates/spec/jobs/mail_delivery_job_spec.rb +11 -0
- data/lib/generators/trek/templates/spec/mailers/admin_mailer_spec.rb +10 -0
- data/lib/generators/trek/templates/spec/requests/admin/fragments_spec.rb +1 -1
- data/lib/generators/trek/templates/spec/requests/admin/good_job_spec.rb +34 -0
- data/lib/generators/trek/templates/spec/requests/admin/pages_spec.rb +1 -1
- data/lib/generators/trek/templates/spec/requests/admin/panels_spec.rb +16 -0
- data/lib/generators/trek/templates/spec/requests/admin/password_renewal_spec.rb +2 -2
- data/lib/generators/trek/templates/spec/requests/admin/sign_in_lockout_spec.rb +1 -1
- data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +2 -2
- data/lib/generators/trek/templates/views/admin/users/index.html.slim +1 -1
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.en.html.slim +1 -1
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.en.text.erb +1 -1
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.fr.html.slim +1 -1
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.fr.text.erb +1 -1
- data/lib/trek/csp_reports.rb +22 -4
- data/lib/trek/typography_formatter.rb +35 -4
- data/lib/trek/version.rb +1 -1
- data/package.json +12 -8
- data/trek.gemspec +1 -1
- data/vitest.config.mjs +24 -0
- data/yarn.lock +1004 -288
- metadata +90 -3
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
= render "<%= accounts_path %>/flash"
|
|
2
|
+
|
|
3
|
+
h1 = t(".title")
|
|
4
|
+
p = t(".intro")
|
|
5
|
+
|
|
6
|
+
= form_with url: <%= route_prefix %>_password_reset_path do |f|
|
|
7
|
+
= f.label :email, <%= class_name %>.human_attribute_name(:email)
|
|
8
|
+
= f.email_field :email,
|
|
9
|
+
value: @email,
|
|
10
|
+
required: true,
|
|
11
|
+
autofocus: true,
|
|
12
|
+
autocomplete: "username"
|
|
13
|
+
= f.submit t(".submit")
|
|
14
|
+
|
|
15
|
+
p = link_to t(".sign_in"), <%= route_prefix %>_sign_in_path
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
= render "<%= accounts_path %>/flash"
|
|
2
|
+
|
|
3
|
+
h1 = t(".title")
|
|
4
|
+
ruby:
|
|
5
|
+
rules = {
|
|
6
|
+
min: @<%= file_name %>.password_min_length,
|
|
7
|
+
passphrase: @<%= file_name %>.passphrase_length
|
|
8
|
+
}
|
|
9
|
+
p = t(".intro", **rules)
|
|
10
|
+
|
|
11
|
+
= form_with model: @<%= file_name %>, url: <%= route_prefix %>_password_path, method: :patch do |f|
|
|
12
|
+
- if @<%= file_name %>.errors.any?
|
|
13
|
+
ul(role="alert")
|
|
14
|
+
- @<%= file_name %>.errors.full_messages.each do |message|
|
|
15
|
+
li = message
|
|
16
|
+
= f.label :password
|
|
17
|
+
= f.password_field :password,
|
|
18
|
+
required: true,
|
|
19
|
+
autofocus: true,
|
|
20
|
+
autocomplete: "new-password",
|
|
21
|
+
maxlength: 72,
|
|
22
|
+
minlength: @<%= file_name %>.password_min_length
|
|
23
|
+
= f.submit t(".submit")
|
|
24
|
+
|
|
25
|
+
p = link_to t(".back"), <%= route_prefix %>_account_path
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
= render "<%= accounts_path %>/flash"
|
|
2
|
+
|
|
3
|
+
h1 = t(".title")
|
|
4
|
+
|
|
5
|
+
= form_with model: @<%= file_name %>, url: <%= route_prefix %>_sign_up_path do |f|
|
|
6
|
+
- if @<%= file_name %>.errors.any?
|
|
7
|
+
ul(role="alert")
|
|
8
|
+
- @<%= file_name %>.errors.full_messages.each do |message|
|
|
9
|
+
li = message
|
|
10
|
+
= f.label :email
|
|
11
|
+
= f.email_field :email, required: true, autofocus: true, autocomplete: "email"
|
|
12
|
+
= f.label :name
|
|
13
|
+
= f.text_field :name, autocomplete: "name"
|
|
14
|
+
= f.label :password
|
|
15
|
+
ruby:
|
|
16
|
+
rules = {
|
|
17
|
+
min: @<%= file_name %>.password_min_length,
|
|
18
|
+
passphrase: @<%= file_name %>.passphrase_length
|
|
19
|
+
}
|
|
20
|
+
p = t(".rules", **rules)
|
|
21
|
+
= f.password_field :password,
|
|
22
|
+
required: true,
|
|
23
|
+
autocomplete: "new-password",
|
|
24
|
+
maxlength: 72,
|
|
25
|
+
minlength: @<%= file_name %>.password_min_length
|
|
26
|
+
= f.submit t(".submit")
|
|
27
|
+
|
|
28
|
+
p = link_to t(".sign_in"), <%= route_prefix %>_sign_in_path
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
= render "<%= accounts_path %>/flash"
|
|
2
|
+
|
|
3
|
+
h1 = t(".title")
|
|
4
|
+
<% if code? -%>
|
|
5
|
+
- validity = distance_of_time_in_words(0, <%= class_name %>::SignInCode.validity)
|
|
6
|
+
p = t(".intro", email: @email, validity:)
|
|
7
|
+
|
|
8
|
+
= form_with url: <%= route_prefix %>_sign_in_code_path do |f|
|
|
9
|
+
= f.label :otp, t(".code")
|
|
10
|
+
= f.text_field :otp,
|
|
11
|
+
required: true,
|
|
12
|
+
autofocus: true,
|
|
13
|
+
autocomplete: "one-time-code",
|
|
14
|
+
inputmode: "numeric",
|
|
15
|
+
pattern: "[0-9 ]*",
|
|
16
|
+
maxlength: 7
|
|
17
|
+
= f.submit t(".submit")
|
|
18
|
+
|
|
19
|
+
p = link_to t(".other_email"), <%= route_prefix %>_sign_in_path
|
|
20
|
+
<% else -%>
|
|
21
|
+
|
|
22
|
+
= form_with url: <%= route_prefix %>_sign_in_path do |f|
|
|
23
|
+
= f.label :email, <%= class_name %>.human_attribute_name(:email)
|
|
24
|
+
= f.email_field :email,
|
|
25
|
+
value: @email,
|
|
26
|
+
required: true,
|
|
27
|
+
autofocus: true,
|
|
28
|
+
autocomplete: "username"
|
|
29
|
+
= f.label :password, <%= class_name %>.human_attribute_name(:password)
|
|
30
|
+
= f.password_field :password,
|
|
31
|
+
required: true,
|
|
32
|
+
autocomplete: "current-password",
|
|
33
|
+
maxlength: 72
|
|
34
|
+
= f.submit t(".submit")
|
|
35
|
+
|
|
36
|
+
p = link_to t(".forgot_password"), new_<%= route_prefix %>_password_reset_path
|
|
37
|
+
p = link_to t(".sign_up"), <%= route_prefix %>_sign_up_path
|
|
38
|
+
<% end -%>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
= render "<%= accounts_path %>/flash"
|
|
2
|
+
|
|
3
|
+
h1 = t(".title")
|
|
4
|
+
p = t(".intro")
|
|
5
|
+
|
|
6
|
+
= form_with url: <%= route_prefix %>_sign_in_path do |f|
|
|
7
|
+
= f.label :email, <%= class_name %>.human_attribute_name(:email)
|
|
8
|
+
= f.email_field :email,
|
|
9
|
+
value: @email,
|
|
10
|
+
required: true,
|
|
11
|
+
autofocus: true,
|
|
12
|
+
autocomplete: "email"
|
|
13
|
+
= f.submit t(".submit")
|
|
14
|
+
<% if password? -%>
|
|
15
|
+
|
|
16
|
+
p = link_to t(".password"), <%= route_prefix %>_password_sign_in_path
|
|
17
|
+
<% end -%>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe MailDeliveryJob, type: :job do
|
|
4
|
+
it "delivers the mail enqueued by deliver_later" do
|
|
5
|
+
expect(ActionMailer::Base.delivery_job).to eq(described_class)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it "retries a failed delivery instead of discarding it" do
|
|
9
|
+
expect(described_class.rescue_handlers.map(&:first)).to include("StandardError")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -15,6 +15,9 @@ RSpec.describe AdminMailer do
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
describe "the new sign-in email" do
|
|
18
|
+
# No DNS lookup in specs: an address without a host name, unless an example gives one
|
|
19
|
+
before { allow(Trek::ReverseDns).to receive(:hostname).and_return(nil) }
|
|
20
|
+
|
|
18
21
|
# In English whatever the app's default locale: the examples read English words
|
|
19
22
|
let(:user) { users.create(role: :editor, locale: "en") }
|
|
20
23
|
let(:mail) do
|
|
@@ -30,6 +33,13 @@ RSpec.describe AdminMailer do
|
|
|
30
33
|
expect(mail.text_part.body.to_s).to include("Firefox on Windows").and include("203.0.113.7")
|
|
31
34
|
end
|
|
32
35
|
|
|
36
|
+
it "names the address's host, as the active sessions do", :aggregate_failures do
|
|
37
|
+
allow(Trek::ReverseDns).to receive(:hostname).with("203.0.113.7").and_return("host.example.net")
|
|
38
|
+
|
|
39
|
+
expect(mail.text_part.body.to_s).to include("203.0.113.7 (host.example.net)")
|
|
40
|
+
expect(mail.html_part.body.to_s).to include("203.0.113.7 (host.example.net)")
|
|
41
|
+
end
|
|
42
|
+
|
|
33
43
|
it "links to the active sessions and to a password reset" do
|
|
34
44
|
expect(mail.html_part.body.to_s).to include("#sessions").and include("user_password_resets/new")
|
|
35
45
|
end
|
|
@@ -53,7 +53,7 @@ RSpec.describe "Admin fragments", type: :request do
|
|
|
53
53
|
it "refuses a key already used in the namespace" do
|
|
54
54
|
post admin_fragments_path, params: { fragment: { key: "footer", namespace: "layout", title: "Another footer" } }
|
|
55
55
|
|
|
56
|
-
expect(response).to have_http_status(:
|
|
56
|
+
expect(response).to have_http_status(:unprocessable_content)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
it "renders the edit form" do
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
# The GoodJob dashboard is mounted outside the admin controllers (config/routes.rb), so the guard
|
|
4
|
+
# lives in config/initializers/good_job.rb. Anyone but an administrator must get a 404.
|
|
5
|
+
RSpec.describe "GoodJob dashboard", type: :request do
|
|
6
|
+
let(:password) { "secret-password-123" }
|
|
7
|
+
|
|
8
|
+
def sign_in(user)
|
|
9
|
+
post admin_user_session_path, params: { email: user.email, password: }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "lets an administrator in" do
|
|
13
|
+
sign_in User.create!(email: "admin@example.com", password:, role: "admin")
|
|
14
|
+
|
|
15
|
+
get "/admin/good_job"
|
|
16
|
+
follow_redirect!
|
|
17
|
+
|
|
18
|
+
expect(response).to have_http_status(:ok)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "hides the dashboard from a privileged user who is not an administrator" do
|
|
22
|
+
sign_in User.create!(email: "editor@example.com", password:, role: "editor")
|
|
23
|
+
|
|
24
|
+
get "/admin/good_job"
|
|
25
|
+
|
|
26
|
+
expect(response).to have_http_status(:not_found)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "hides the dashboard from a visitor" do
|
|
30
|
+
get "/admin/good_job"
|
|
31
|
+
|
|
32
|
+
expect(response).to have_http_status(:not_found)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -34,7 +34,7 @@ RSpec.describe "Admin pages", type: :request do
|
|
|
34
34
|
it "refuses a page without a title" do
|
|
35
35
|
post admin_pages_path, params: { page: { title: "" } }
|
|
36
36
|
|
|
37
|
-
expect(response).to have_http_status(:
|
|
37
|
+
expect(response).to have_http_status(:unprocessable_content)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
it "renders the edit form" do
|
|
@@ -51,4 +51,20 @@ RSpec.describe "Admin content editor panels", type: :request do
|
|
|
51
51
|
|
|
52
52
|
expect(response).to have_http_status(:ok)
|
|
53
53
|
end
|
|
54
|
+
|
|
55
|
+
it "asks for a link's title, and gives it to the editor", :aggregate_failures do
|
|
56
|
+
get new_admin_panels_link_path(link: { turbo_frame_id: "page_1_content" })
|
|
57
|
+
expect(stacked_panel.at_css("input[name='link[title]']")).to be_present
|
|
58
|
+
|
|
59
|
+
post admin_panels_links_path(format: :turbo_stream),
|
|
60
|
+
params: { link: { turbo_frame_id: "page_1_content", href: "https://example.com/", title: "An example" } }
|
|
61
|
+
expect(response.body).to include(""title":"An example"")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "inserts a call to action into the editor" do
|
|
65
|
+
post admin_panels_prompts_path(format: :turbo_stream),
|
|
66
|
+
params: { prompt: { turbo_frame_id: "page_1_content", href: "https://example.com/", text: "Sign up" } }
|
|
67
|
+
|
|
68
|
+
expect(response.body).to include("<prompts-block", "Sign up")
|
|
69
|
+
end
|
|
54
70
|
end
|
|
@@ -12,7 +12,7 @@ RSpec.describe "Admin password renewal", type: :request do
|
|
|
12
12
|
editor
|
|
13
13
|
sign_in(admin)
|
|
14
14
|
|
|
15
|
-
post
|
|
15
|
+
post admin_password_renewal_path
|
|
16
16
|
|
|
17
17
|
expect(editor.reload.password_renewal_required).to be(true)
|
|
18
18
|
end
|
|
@@ -21,7 +21,7 @@ RSpec.describe "Admin password renewal", type: :request do
|
|
|
21
21
|
admin
|
|
22
22
|
sign_in(editor)
|
|
23
23
|
|
|
24
|
-
post
|
|
24
|
+
post admin_password_renewal_path
|
|
25
25
|
|
|
26
26
|
expect(admin.reload.password_renewal_required).to be(false)
|
|
27
27
|
end
|
|
@@ -24,7 +24,7 @@ RSpec.describe "Admin sign-in lockout", type: :request do
|
|
|
24
24
|
|
|
25
25
|
sign_in
|
|
26
26
|
|
|
27
|
-
expect(response).to have_http_status(:
|
|
27
|
+
expect(response).to have_http_status(:unprocessable_content)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
it "answers a locked account as a wrong password, so it can't be told apart" do
|
|
@@ -31,7 +31,7 @@ RSpec.describe "Admin sign-in", type: :request do
|
|
|
31
31
|
it "refuses a wrong password" do
|
|
32
32
|
post admin_user_session_path, params: { email: editor.email, password: "not the password" }
|
|
33
33
|
|
|
34
|
-
expect(response).to have_http_status(:
|
|
34
|
+
expect(response).to have_http_status(:unprocessable_content)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
it "keeps the email typed after a wrong password" do
|
|
@@ -44,7 +44,7 @@ RSpec.describe "Admin sign-in", type: :request do
|
|
|
44
44
|
reader = users.create(role: :reader)
|
|
45
45
|
post admin_user_session_path, params: { email: reader.email, password: users.password }
|
|
46
46
|
|
|
47
|
-
expect(response).to have_http_status(:
|
|
47
|
+
expect(response).to have_http_status(:unprocessable_content)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
it "answers an unknown email as a wrong password, so it can't be used to find accounts" do
|
data/lib/trek/csp_reports.rb
CHANGED
|
@@ -16,7 +16,9 @@ module Trek
|
|
|
16
16
|
# report, it must be plausible too: a known directive, and a keyword or a host name blocked.
|
|
17
17
|
#
|
|
18
18
|
# A violation keeps the page's path, without its query, which may carry a token, and the blocked
|
|
19
|
-
# resource's origin.
|
|
19
|
+
# resource's origin. To tell what was blocked, it also keeps where (the source file's URL, without
|
|
20
|
+
# its query, and the line), and the sample: the start of the inline script or style, which browsers
|
|
21
|
+
# only send under `'report-sample'`.
|
|
20
22
|
module CspReports
|
|
21
23
|
EXTENSION_SCHEMES = %w[chrome-extension moz-extension safari-extension safari-web-extension ms-browser-extension].freeze
|
|
22
24
|
|
|
@@ -32,9 +34,13 @@ module Trek
|
|
|
32
34
|
SCHEMES = %w[http https ws wss].freeze
|
|
33
35
|
HOST_NAME = /\A(?=.{1,253}\z)[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\z/i
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
# Browsers send 40 characters at most; a forged report could send any number
|
|
38
|
+
SAMPLE_LENGTH = 40
|
|
39
|
+
|
|
40
|
+
Violation = Struct.new(:directive, :blocked, :page, :disposition, :source, :sample, keyword_init: true) do
|
|
36
41
|
def to_s
|
|
37
|
-
|
|
42
|
+
# The sample inspected, as it may hold a line break
|
|
43
|
+
"#{directive} blocked #{blocked} on #{page} (#{disposition})#{" at #{source}" if source}#{": #{sample.inspect}" if sample}"
|
|
38
44
|
end
|
|
39
45
|
end
|
|
40
46
|
|
|
@@ -68,11 +74,23 @@ module Trek
|
|
|
68
74
|
directive:,
|
|
69
75
|
blocked:,
|
|
70
76
|
page: page.path.presence || "/",
|
|
71
|
-
disposition: fields["disposition"] || "enforce"
|
|
77
|
+
disposition: fields["disposition"] || "enforce",
|
|
78
|
+
source: location(source, fields["line-number"] || fields["lineNumber"]),
|
|
79
|
+
sample: (fields["script-sample"] || fields["sample"]).to_s.first(SAMPLE_LENGTH).presence
|
|
72
80
|
)
|
|
73
81
|
end
|
|
74
82
|
private_class_method :violation
|
|
75
83
|
|
|
84
|
+
# Where the violation happened: the source file's origin and path, and the line
|
|
85
|
+
def self.location(source, line)
|
|
86
|
+
uri = parse_uri(source)
|
|
87
|
+
return unless uri&.host && (base = origin(source))
|
|
88
|
+
|
|
89
|
+
line = Integer(line, exception: false)
|
|
90
|
+
"#{base}#{uri.path}#{":#{line}" if line}"
|
|
91
|
+
end
|
|
92
|
+
private_class_method :location
|
|
93
|
+
|
|
76
94
|
# A URL's origin, or the keyword a browser reports instead ("inline", "eval", "data"…); nil when
|
|
77
95
|
# it's neither
|
|
78
96
|
def self.origin(value)
|
|
@@ -1,20 +1,33 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
1
3
|
module Trek
|
|
2
4
|
# `TypographyFormatter` formats strings to enforce French typography rules.
|
|
3
5
|
#
|
|
4
6
|
# Supports both plain strings and ProseMirror JSONB hashes. When given
|
|
5
7
|
# a Hash, it walks the node tree and formats only `text` node values
|
|
6
|
-
# in place, leaving the structure intact.
|
|
8
|
+
# in place, leaving the structure intact. A ProseMirror document given
|
|
9
|
+
# as a JSON string, as the content editor submits it, is parsed first,
|
|
10
|
+
# and returned as a Hash.
|
|
7
11
|
class TypographyFormatter
|
|
8
12
|
NBSP = [160].pack("U*").freeze
|
|
9
13
|
NNBSP = [8239].pack("U*").freeze # U+202F narrow no-break space
|
|
14
|
+
# Signs preceded by a non-breaking space, whether one was typed or not
|
|
10
15
|
SPACE_BEFORE_CHARS = [
|
|
11
|
-
[NBSP, %w[
|
|
16
|
+
[NBSP, %w[» – —]]
|
|
17
|
+
].freeze
|
|
18
|
+
# Signs preceded by a non-breaking space when one was typed, or when they end a word: followed by a
|
|
19
|
+
# space, punctuation or the end of the text. Inside a word, as in a time (10:30), a URL
|
|
20
|
+
# (https://example.com/?a=50%25) or a ratio (3:1), they aren't punctuation, and are left alone.
|
|
21
|
+
SPACE_BEFORE_WORD_ENDING_CHARS = [
|
|
22
|
+
[NBSP, %w[: % € \$]],
|
|
12
23
|
[NNBSP, %w[; ! \?]]
|
|
13
24
|
].freeze
|
|
25
|
+
WORD_ENDING_SIGNS = %w[: ; ! \?].freeze
|
|
14
26
|
NBSP_AFTER_CHARS = %w[« – —].freeze
|
|
15
27
|
NBSP_BEFORE_UNITS = %w[°C °F g h ha kg km Ko ko kW L l m min ml Mo mo s].freeze
|
|
16
28
|
|
|
17
29
|
def initialize(input)
|
|
30
|
+
input = JSON.parse(input) if prosemirror_json?(input)
|
|
18
31
|
@source = input
|
|
19
32
|
@prosemirror = input.is_a?(Hash)
|
|
20
33
|
@output = @prosemirror ? deep_dup_hash(input) : input.dup
|
|
@@ -34,6 +47,11 @@ module Trek
|
|
|
34
47
|
|
|
35
48
|
private
|
|
36
49
|
|
|
50
|
+
# Formatted as plain text, the JSON would get non-breaking spaces before its colons, and no longer parse
|
|
51
|
+
def prosemirror_json?(input)
|
|
52
|
+
input.is_a?(String) && input.start_with?('{"type":"doc"')
|
|
53
|
+
end
|
|
54
|
+
|
|
37
55
|
def format_string!
|
|
38
56
|
remove_double_spaces
|
|
39
57
|
add_nbsp
|
|
@@ -79,6 +97,18 @@ module Trek
|
|
|
79
97
|
regexp = /(?<=\S)(\p{Zs}?(#{chars.join("|")}))/
|
|
80
98
|
@output.gsub!(regexp, "#{space}\\2")
|
|
81
99
|
end
|
|
100
|
+
|
|
101
|
+
word_end = "\\p{Zs}|\\z|[#{WORD_ENDING_SIGNS.join}.,…»”)\\]]"
|
|
102
|
+
# Not after another sign: "Quoi?!" gets one space, before the group
|
|
103
|
+
not_after_sign = "(?<![\\p{Zs}\\s#{WORD_ENDING_SIGNS.join}])"
|
|
104
|
+
|
|
105
|
+
SPACE_BEFORE_WORD_ENDING_CHARS.each do |space, chars|
|
|
106
|
+
signs = chars.join("|")
|
|
107
|
+
# Repairs what earlier versions spaced inside a word: "10 :30", "https ://"
|
|
108
|
+
@output.gsub!(/[#{NBSP}#{NNBSP}](#{signs})(?!#{word_end})/, "\\1")
|
|
109
|
+
@output.gsub!(/(?<=\S)\p{Zs}+(#{signs})(?=#{word_end})/, "#{space}\\1")
|
|
110
|
+
@output.gsub!(/#{not_after_sign}(#{signs})(?=#{word_end})/, "#{space}\\1")
|
|
111
|
+
end
|
|
82
112
|
end
|
|
83
113
|
|
|
84
114
|
def add_nbsp_before_units
|
|
@@ -87,7 +117,7 @@ module Trek
|
|
|
87
117
|
end
|
|
88
118
|
|
|
89
119
|
def add_nbsp_after_chars
|
|
90
|
-
regexp = /((#{NBSP_AFTER_CHARS.join("|")})\p{Zs}?)(
|
|
120
|
+
regexp = /((#{NBSP_AFTER_CHARS.join("|")})\p{Zs}?)(?=[\p{L}\p{N}])/
|
|
91
121
|
@output.gsub!(regexp, "\\2#{NBSP}")
|
|
92
122
|
end
|
|
93
123
|
|
|
@@ -109,7 +139,8 @@ module Trek
|
|
|
109
139
|
end
|
|
110
140
|
|
|
111
141
|
def replace_apostrophes
|
|
112
|
-
|
|
142
|
+
# Letters with accents too (l'\u00e9t\u00e9), which `\w` doesn't match
|
|
143
|
+
@output.gsub!(/(?<=[\p{L}\p{N}])'(?=[\p{L}\p{N}])/, "\u2019")
|
|
113
144
|
end
|
|
114
145
|
|
|
115
146
|
def remove_spaces_before_attached_punctuation
|
data/lib/trek/version.rb
CHANGED
data/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trek",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "A modern content management & back-office system for Ruby on Rails",
|
|
5
5
|
"homepage": "https://trek.etamin.studio",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,16 +20,15 @@
|
|
|
20
20
|
},
|
|
21
21
|
"author": "Etamin Studio",
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@floating-ui/dom": "^1.8.0",
|
|
23
24
|
"@fontsource/inter": "^5.3.0",
|
|
24
25
|
"@hotwired/stimulus": "^3.2.2",
|
|
25
26
|
"@hotwired/turbo-rails": "^8.0.20",
|
|
26
27
|
"@radix-ui/colors": "^3.0.0",
|
|
27
|
-
"@tiptap/core": "^
|
|
28
|
-
"@tiptap/extension-floating-menu": "^
|
|
29
|
-
"@tiptap/
|
|
30
|
-
"@tiptap/
|
|
31
|
-
"@tiptap/pm": "^2.1.11",
|
|
32
|
-
"@tiptap/starter-kit": "^2.1.11",
|
|
28
|
+
"@tiptap/core": "^3.32.0",
|
|
29
|
+
"@tiptap/extension-floating-menu": "^3.32.0",
|
|
30
|
+
"@tiptap/pm": "^3.32.0",
|
|
31
|
+
"@tiptap/starter-kit": "^3.32.0",
|
|
33
32
|
"@uppy/core": "^3.0.4",
|
|
34
33
|
"@uppy/locales": "^3.0.1",
|
|
35
34
|
"@uppy/thumbnail-generator": "^3.0.2",
|
|
@@ -42,11 +41,13 @@
|
|
|
42
41
|
"tom-select": "^2.2.2"
|
|
43
42
|
},
|
|
44
43
|
"devDependencies": {
|
|
44
|
+
"@vitest/browser-playwright": "^5.0.5",
|
|
45
45
|
"esbuild": "^0.28.0",
|
|
46
46
|
"esbuild-rails": "^1.0.7",
|
|
47
47
|
"esbuild-style-plugin": "^1.6.3",
|
|
48
48
|
"eslint": "^8.49.0",
|
|
49
49
|
"eslint-config-prettier": "^8.10.2",
|
|
50
|
+
"playwright": "^1.64.1",
|
|
50
51
|
"postcss": "^8.4.30",
|
|
51
52
|
"postcss-flexbugs-fixes": "^5.0.2",
|
|
52
53
|
"postcss-import": "^15.1.0",
|
|
@@ -57,11 +58,14 @@
|
|
|
57
58
|
"stylelint": "^15.11.0",
|
|
58
59
|
"stylelint-config-standard": "^31.0.0",
|
|
59
60
|
"stylelint-order": "^6.0.4",
|
|
61
|
+
"vite": "^8.3.5",
|
|
60
62
|
"vitepress": "^1.6.4",
|
|
61
|
-
"vitepress-plugin-llms": "^1.13.1"
|
|
63
|
+
"vitepress-plugin-llms": "^1.13.1",
|
|
64
|
+
"vitest": "^5.0.5"
|
|
62
65
|
},
|
|
63
66
|
"packageManager": "yarn@4.18.1",
|
|
64
67
|
"scripts": {
|
|
68
|
+
"test": "vitest run",
|
|
65
69
|
"docs:dev": "vitepress dev docs",
|
|
66
70
|
"docs:build": "vitepress build docs",
|
|
67
71
|
"docs:preview": "vitepress preview docs"
|
data/trek.gemspec
CHANGED
|
@@ -50,7 +50,7 @@ Gem::Specification.new do |spec|
|
|
|
50
50
|
spec.add_dependency "mobility", "~> 1.3.2"
|
|
51
51
|
spec.add_dependency "phonelib", "~> 0.8"
|
|
52
52
|
spec.add_dependency "postmark-rails"
|
|
53
|
-
spec.add_dependency "prosemirror", ">= 0
|
|
53
|
+
spec.add_dependency "prosemirror", ">= 1.0", "< 2"
|
|
54
54
|
spec.add_dependency "rails", [">= 8.0", "< 8.2"]
|
|
55
55
|
spec.add_dependency "rails-healthcheck"
|
|
56
56
|
spec.add_dependency "rails-i18n", ">= 6.0"
|
data/vitest.config.mjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
import { playwright } from "@vitest/browser-playwright";
|
|
3
|
+
|
|
4
|
+
// JavaScript tests (yarn test): the admin's Stimulus controllers, with their libraries, in a headless Chrome.
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
test: {
|
|
7
|
+
include: ["spec/javascript/**/*.test.js"],
|
|
8
|
+
browser: {
|
|
9
|
+
enabled: true,
|
|
10
|
+
headless: true,
|
|
11
|
+
// The Chrome installed on the machine, as the system specs use: Playwright downloads none. CI's image
|
|
12
|
+
// has Debian's Chromium instead, at CHROME_PATH.
|
|
13
|
+
provider: playwright({
|
|
14
|
+
launchOptions: process.env.CHROME_PATH
|
|
15
|
+
? { executablePath: process.env.CHROME_PATH }
|
|
16
|
+
: { channel: "chrome" },
|
|
17
|
+
}),
|
|
18
|
+
instances: [{ browser: "chromium" }],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
// The components import their CSS, which the tests don't need: without this, Vite would load
|
|
22
|
+
// postcss.config.js, written for the esbuild build.
|
|
23
|
+
css: { postcss: {} },
|
|
24
|
+
});
|