trek 2.3.1 → 2.4.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.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/.node-version +1 -1
  3. data/.yarn/releases/yarn-4.18.1.cjs +1000 -0
  4. data/.yarnrc.yml +3 -5
  5. data/AGENTS.md +7 -4
  6. data/CHANGELOG.md +55 -0
  7. data/Gemfile.lock +35 -1
  8. data/Makefile +12 -0
  9. data/app/assets/icons/trek/passkey.svg +5 -0
  10. data/app/assets/stylesheets/trek/base.css +6 -0
  11. data/app/components/trek/entries/item_component/item_component.css +6 -0
  12. data/app/components/trek/form/image_field_component/image_field_component.html.slim +9 -0
  13. data/app/components/trek/form/password_strength_component/password_strength_component.css +56 -0
  14. data/app/components/trek/form/password_strength_component/password_strength_component.html.slim +8 -0
  15. data/app/components/trek/form/password_strength_component/password_strength_component.js +74 -0
  16. data/app/components/trek/form/password_strength_component.rb +29 -0
  17. data/app/components/trek/gate_component/gate_component.css +7 -0
  18. data/app/components/trek/icon/sprite_component.rb +3 -2
  19. data/app/components/trek/section_component/section_component.css +22 -0
  20. data/app/components/trek/section_component/section_component.html.slim +5 -0
  21. data/app/components/trek/section_component/section_component.js +1 -0
  22. data/app/components/trek/section_component.rb +14 -0
  23. data/app/controllers/concerns/trek/auditable.rb +103 -0
  24. data/app/controllers/concerns/trek/filters.rb +2 -0
  25. data/app/controllers/concerns/trek/passkeys.rb +37 -0
  26. data/app/controllers/concerns/trek/scopes.rb +9 -2
  27. data/app/controllers/trek/csp_reports_controller.rb +62 -0
  28. data/app/controllers/trek/resource_controller.rb +7 -1
  29. data/app/helpers/trek/audit_logs_helper.rb +21 -0
  30. data/app/helpers/trek/user_agents_helper.rb +36 -0
  31. data/app/javascript/controllers/index.js +3 -0
  32. data/app/javascript/controllers/passkey_controller.js +118 -0
  33. data/app/javascript/trek.js +4 -0
  34. data/app/jobs/trek/audit_log/purge_job.rb +18 -0
  35. data/app/jobs/trek/purge_expired_sessions_job.rb +14 -0
  36. data/app/jobs/trek/resolve_session_hostname_job.rb +17 -0
  37. data/app/models/concerns/trek/audit_log.rb +30 -0
  38. data/app/models/concerns/trek/users/passkeys.rb +75 -0
  39. data/app/models/concerns/trek/users/password_history.rb +51 -0
  40. data/app/models/concerns/trek/users/password_policy.rb +85 -0
  41. data/app/models/concerns/trek/users/password_renewal.rb +51 -0
  42. data/app/models/concerns/trek/users/session_lifetime.rb +39 -0
  43. data/app/models/concerns/trek/users/sign_in_lockout.rb +67 -0
  44. data/app/models/concerns/trek/users/sign_ins.rb +31 -0
  45. data/app/models/concerns/trek/users/validations.rb +3 -2
  46. data/app/models/trek/feature.rb +6 -0
  47. data/app/models/trek/pwned_passwords.rb +40 -0
  48. data/app/models/trek/user_session.rb +10 -0
  49. data/app/policies/trek/audit_log_policy.rb +34 -0
  50. data/app/policies/trek/user_policy.rb +5 -0
  51. data/app/validators/not_disposable_validator.rb +18 -0
  52. data/config/brakeman.ignore +22 -0
  53. data/config/i18n-tasks.yml +7 -0
  54. data/config/locales/trek.en.yml +144 -39
  55. data/config/locales/trek.fr.yml +145 -40
  56. data/docs/.vitepress/config.mjs +8 -3
  57. data/docs/deploy.md +2 -0
  58. data/docs/proxies.md +53 -0
  59. data/docs/reference/components.md +1 -0
  60. data/docs/reference/controllers.md +1 -1
  61. data/docs/reference/environment-variables.md +2 -0
  62. data/docs/reference/generators/admin-user.md +1 -1
  63. data/docs/reference/generators/audit-log.md +113 -0
  64. data/docs/reference/generators/install.md +12 -3
  65. data/docs/reference/generators/security-headers.md +109 -0
  66. data/docs/reference/index.md +3 -0
  67. data/docs/reference/models/tag.md +4 -2
  68. data/docs/reference/models/user.md +123 -3
  69. data/docs/reference/validators.md +94 -0
  70. data/docs/security.md +90 -0
  71. data/lefthook.yml +8 -0
  72. data/lib/generators/trek/install/admin/user_password_resets_generator.rb +5 -0
  73. data/lib/generators/trek/install/admin/user_sessions_generator.rb +5 -0
  74. data/lib/generators/trek/install/admin/users_generator.rb +23 -0
  75. data/lib/generators/trek/install/audit_log_generator.rb +56 -0
  76. data/lib/generators/trek/install/authentication_generator.rb +77 -3
  77. data/lib/generators/trek/install/esbuild_generator.rb +1 -2
  78. data/lib/generators/trek/install/mailer_generator.rb +18 -0
  79. data/lib/generators/trek/install/model_specs_generator.rb +6 -4
  80. data/lib/generators/trek/install/postcss_generator.rb +1 -4
  81. data/lib/generators/trek/install/security_headers_generator.rb +49 -0
  82. data/lib/generators/trek/install/yarn_generator.rb +1 -1
  83. data/lib/generators/trek/install_generator.rb +62 -54
  84. data/lib/generators/trek/taxonomies_generator.rb +5 -0
  85. data/lib/generators/trek/templates/.gitlab-ci.yml +18 -2
  86. data/lib/generators/trek/templates/controllers/admin/active_sessions_controller.rb +21 -0
  87. data/lib/generators/trek/templates/controllers/admin/audit_logs_controller.rb +30 -0
  88. data/lib/generators/trek/templates/controllers/admin/passkey_sessions_controller.rb +41 -0
  89. data/lib/generators/trek/templates/controllers/admin/passkeys_controller.rb +37 -0
  90. data/lib/generators/trek/templates/controllers/admin/user_password_resets_controller.rb +4 -4
  91. data/lib/generators/trek/templates/controllers/admin/user_sessions_controller.rb +53 -4
  92. data/lib/generators/trek/templates/controllers/admin/users_controller.rb +20 -0
  93. data/lib/generators/trek/templates/controllers/concerns/trek/authentication.rb +58 -7
  94. data/lib/generators/trek/templates/helpers/application_helper.rb +0 -4
  95. data/lib/generators/trek/templates/initializers/audit_log.rb +11 -0
  96. data/lib/generators/trek/templates/initializers/content_security_policy.rb +49 -0
  97. data/lib/generators/trek/templates/initializers/permissions_policy.rb +19 -0
  98. data/lib/generators/trek/templates/jobs/admin/password_reset_job.rb +9 -0
  99. data/lib/generators/trek/templates/layouts/admin/auth.html.slim +0 -5
  100. data/lib/generators/trek/templates/layouts/admin.html.slim +0 -3
  101. data/lib/generators/trek/templates/mailers/admin/passwords_mailer.rb +1 -1
  102. data/lib/generators/trek/templates/mailers/admin_mailer.rb +28 -1
  103. data/lib/generators/trek/templates/migrations/audit_logs.rb.erb +27 -0
  104. data/lib/generators/trek/templates/migrations/tags.rb.erb +6 -1
  105. data/lib/generators/trek/templates/migrations/user_passkeys.rb.erb +24 -0
  106. data/lib/generators/trek/templates/migrations/user_password_renewal.rb.erb +8 -0
  107. data/lib/generators/trek/templates/migrations/user_previous_passwords.rb.erb +10 -0
  108. data/lib/generators/trek/templates/migrations/user_session_tracking.rb.erb +11 -0
  109. data/lib/generators/trek/templates/migrations/user_sign_in_lockout.rb.erb +7 -0
  110. data/lib/generators/trek/templates/migrations/user_sign_ins.rb.erb +13 -0
  111. data/lib/generators/trek/templates/models/audit_log.rb +3 -0
  112. data/lib/generators/trek/templates/models/tag_category.rb +2 -1
  113. data/lib/generators/trek/templates/models/user/passkey.rb +17 -0
  114. data/lib/generators/trek/templates/models/user/previous_password.rb +4 -0
  115. data/lib/generators/trek/templates/models/user/session.rb +8 -0
  116. data/lib/generators/trek/templates/policies/audit_log.rb +4 -0
  117. data/lib/generators/trek/templates/spec/mailers/admin/passwords_mailer_spec.rb +16 -0
  118. data/lib/generators/trek/templates/spec/mailers/admin_mailer_spec.rb +50 -0
  119. data/lib/generators/trek/templates/spec/models/audit_log_spec.rb +43 -0
  120. data/lib/generators/trek/templates/spec/models/tag_spec.rb +52 -0
  121. data/lib/generators/trek/templates/spec/models/user/session_spec.rb +68 -0
  122. data/lib/generators/trek/templates/spec/models/user_spec.rb +142 -4
  123. data/lib/generators/trek/templates/spec/requests/admin/active_sessions_spec.rb +119 -0
  124. data/lib/generators/trek/templates/spec/requests/admin/audit_logs_spec.rb +149 -0
  125. data/lib/generators/trek/templates/spec/requests/admin/passkey_sessions_spec.rb +103 -0
  126. data/lib/generators/trek/templates/spec/requests/admin/passkeys_spec.rb +104 -0
  127. data/lib/generators/trek/templates/spec/requests/admin/password_renewal_spec.rb +83 -0
  128. data/lib/generators/trek/templates/spec/requests/admin/sign_in_lockout_spec.rb +95 -0
  129. data/lib/generators/trek/templates/spec/requests/admin/sign_in_notices_spec.rb +93 -0
  130. data/lib/generators/trek/templates/spec/requests/admin/user_password_resets_spec.rb +55 -6
  131. data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +31 -2
  132. data/lib/generators/trek/templates/views/admin/audit_logs/index.html.slim +35 -0
  133. data/lib/generators/trek/templates/views/admin/audit_logs/show.html.slim +50 -0
  134. data/lib/generators/trek/templates/views/admin/user_password_resets/edit.html.slim +17 -7
  135. data/lib/generators/trek/templates/views/admin/user_password_resets/new.html.slim +3 -2
  136. data/lib/generators/trek/templates/views/admin/user_sessions/new.html.slim +28 -2
  137. data/lib/generators/trek/templates/views/admin/users/_passkeys.html.slim +67 -0
  138. data/lib/generators/trek/templates/views/admin/users/_properties.html.slim +21 -0
  139. data/lib/generators/trek/templates/views/admin/users/_sessions.html.slim +70 -0
  140. data/lib/generators/trek/templates/views/admin/users/index.html.slim +13 -0
  141. data/lib/generators/trek/templates/views/admin/users/show.html.slim +10 -21
  142. data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.en.html.slim +22 -0
  143. data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.en.text.erb +16 -0
  144. data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.fr.html.slim +22 -0
  145. data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.fr.text.erb +16 -0
  146. data/lib/generators/trek/templates/views/admin_mailer/sign_in_locked.en.html.slim +17 -0
  147. data/lib/generators/trek/templates/views/admin_mailer/sign_in_locked.en.text.erb +10 -0
  148. data/lib/generators/trek/templates/views/admin_mailer/sign_in_locked.fr.html.slim +18 -0
  149. data/lib/generators/trek/templates/views/admin_mailer/sign_in_locked.fr.text.erb +10 -0
  150. data/lib/trek/admin_ip_allowlist.rb +52 -0
  151. data/lib/trek/csp_reports.rb +99 -0
  152. data/lib/trek/data/common_passwords.txt +857 -0
  153. data/lib/trek/data/passkey_providers.json +404 -0
  154. data/lib/trek/disposable_email_domains.rb +53 -0
  155. data/lib/trek/engine.rb +12 -0
  156. data/lib/trek/passkey_providers.rb +19 -0
  157. data/lib/trek/reverse_dns.rb +27 -0
  158. data/lib/trek/version.rb +1 -1
  159. data/lib/trek.rb +13 -0
  160. data/package.json +3 -5
  161. data/renovate.json +11 -1
  162. data/trek.gemspec +3 -0
  163. data/yarn.lock +1757 -2019
  164. metadata +132 -4
  165. data/.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs +0 -541
  166. data/.yarn/releases/yarn-3.8.0.cjs +0 -875
  167. data/lib/generators/trek/templates/models/user_session.rb +0 -3
data/.yarnrc.yml CHANGED
@@ -1,7 +1,5 @@
1
- nodeLinker: node-modules
1
+ enableGlobalCache: false
2
2
 
3
- plugins:
4
- - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
5
- spec: "@yarnpkg/plugin-interactive-tools"
3
+ nodeLinker: node-modules
6
4
 
7
- yarnPath: .yarn/releases/yarn-3.8.0.cjs
5
+ yarnPath: .yarn/releases/yarn-4.18.1.cjs
data/AGENTS.md CHANGED
@@ -9,7 +9,7 @@ Trek is a customizable CMS for Ruby on Rails, shipped as a **Ruby gem** (`trek`)
9
9
  ## Tech stack
10
10
 
11
11
  - **Ruby** `>= 3.1` (RuboCop targets 3.4) · **Rails** `>= 6.0, < 8.2` (lockfile 8.1.3; host apps need 7.0+)
12
- - **Node** 22.21.1 (`.node-version`) · **Yarn** 3.8.0 (berry) · **PostgreSQL** 12+ · **libvips** for images
12
+ - **Node** 24.22.0 (`.node-version`) · **Yarn** 4.18.1 with its defaults: no install scripts, no package published less than a day ago · **PostgreSQL** 12+ · **libvips** for images
13
13
  - Backend: `action_policy`, `closure_tree`, `acts_as_list`, `mobility` + `i18n-tasks`, `shrine` (+ blurhash/image_processing/aws-sdk-s3), `view_component` + `slim-rails` + `lookbook`, `kaminari`, `deepl-rb`
14
14
  - Frontend: **esbuild** (`esbuild.config.js`) + **PostCSS** (`postcss.config.js`); Stimulus/Turbo, TipTap, Uppy, Tom Select, SortableJS
15
15
  - Tests: **RSpec** (`rspec-rails`, capybara, simplecov) · Docs: **VitePress** (`docs/`)
@@ -23,7 +23,8 @@ rake # default: spec + rubocop
23
23
  rake spec # run tests (or: bundle exec rspec)
24
24
  COVERAGE=1 bundle exec rspec # tests + SimpleCov report in coverage/ (fails under the minimum set in spec/spec_helper.rb)
25
25
  make lint # all linters below
26
- make fix # rubocop/eslint/stylelint/prettier autocorrect
26
+ make fix # rubocop/eslint/stylelint/prettier autocorrect, then make i18n-normalize
27
+ make i18n-normalize # sort the locale keys (i18n-tasks normalize), then Prettier them; CI checks it
27
28
  make release # bump version (version.rb + package.json), stamp CHANGELOG, tag, push gem + npm
28
29
  yarn docs:dev # VitePress docs locally
29
30
  ```
@@ -42,14 +43,14 @@ Security: `bundle exec brakeman` · `bundle exec bundle-audit check --update`
42
43
 
43
44
  ## Structure
44
45
 
45
- - `app/` — engine code, namespaced under `trek/`: `components/` (ViewComponents), `controllers/`, `forms/`, `models/`, `policies/` (ActionPolicy), `uploaders/` (Shrine), `views/`, `javascript/` (Stimulus)
46
+ - `app/` — engine code, namespaced under `trek/`: `components/` (ViewComponents), `controllers/`, `forms/`, `models/`, `policies/` (ActionPolicy), `uploaders/` (Shrine), `validators/` (top-level, e.g. `NotDisposableValidator`), `views/`, `javascript/` (Stimulus)
46
47
  - `lib/generators/trek/` — the heart of the gem: `install/` (~30 generators), `scaffold_generator.rb`, `admin/`, `templates/`
47
48
  - `lib/trek/` — `engine.rb`, `version.rb`, `i18n_keys_resolver.rb`, `typography_formatter.rb`
48
49
  - `config/locales/` — `trek.en.yml`, `trek.fr.yml`
49
50
  - `spec/` — RSpec suite, two kinds of spec:
50
51
  - **isolated** (`require_relative` the file under test, stub its host): concerns, helpers, simple components
51
52
  - **Rails** (`require "rails_helper"`): boots `spec/dummy`, a minimal host app on PostgreSQL (`trek_test`, or `DATABASE_URL`). Its models, migrations, Shrine initializer and the whole admin (controllers, views, routes, policies, mailers) are not committed: `spec/dummy/lib/generated_files.rb` runs Trek's own generator steps into `tmp/dummy` on every boot, and the database is rebuilt from them. A new model or migration template must be added to its `STEPS`, or `spec/dummy/generated_files_spec.rb` fails.
52
- - **Host-app specs**: model and admin request specs and their [Oaken](https://github.com/kaspth/oaken) seeds are templates (`lib/generators/trek/templates/{spec/models,spec/requests,db/seeds}/`), installed in host apps by `trek:install:model_specs` and run by Trek's suite from `tmp/dummy` (`spec/dummy/generated_specs_spec.rb`); test data comes from `users.create`, `pages.create` and `seed "cases/…"`, not factories. Write specs for Trek's models there, in host-app style (no `frozen_string_literal`, Ruby 3.1-compatible — no bare `it` block parameter), so host apps get them too.
53
+ - **Host-app specs**: model, admin request, policy and mailer specs and their [Oaken](https://github.com/kaspth/oaken) seeds are templates (`lib/generators/trek/templates/{spec/models,spec/requests,spec/policies,spec/mailers,db/seeds}/`), installed in host apps by `trek:install:model_specs` and run by Trek's suite from `tmp/dummy` (`spec/dummy/generated_specs_spec.rb`); test data comes from `users.create`, `pages.create` and `seed "cases/…"`, not factories. Write specs for Trek's models there, in host-app style (no `frozen_string_literal`, Ruby 3.1-compatible — no bare `it` block parameter), so host apps get them too.
53
54
  - **Generators** (`spec/trek/generators_spec.rb`): every generator runs on a copy of a real `rails new` app (`spec/support/generators.rb`, built once per run into `tmp/generator_skeleton`), then again to check it changes nothing. Shell-outs are recorded in `shell_commands`, not run. A new generator is picked up automatically; if it needs arguments, an earlier Trek step or a starting file, add it to the spec's `arguments`, `prerequisites` or `setups`.
54
55
  - Both kinds run in one process, so an isolated spec must restore any global it changes (`I18n`, `Shrine` storages…). `spec_helper` loads Rails and Action View before Trek, as a host app does: several dependencies (ActionPolicy, Slim, ViewComponent) configure themselves from what is already loaded.
55
56
  - `docs/` — VitePress site
@@ -61,5 +62,7 @@ Security: `bundle exec brakeman` · `bundle exec bundle-audit check --update`
61
62
  - **i18n**: keep English + French locales in sync (`config/locales/trek.*.yml`); DeepL auto-translation via `DEEPL_AUTH_KEY`
62
63
  - **Versioning**: version lives in both `lib/trek/version.rb` and `package.json` — keep in sync via `make release`
63
64
  - **CHANGELOG.md**: Keep-a-Changelog format. **Every MR must add an `[Unreleased]` entry.** Keep entries short: one or two sentences on what changed for host apps, not how it was found or fixed — the MR description holds the details. When the change requires action in the host app on upgrade, end the entry with a bold `**Upgrade note:**` label followed by what to do. Use the same label when the change lets host apps remove or simplify existing patches or custom implementations (e.g. "**Upgrade note:** you can now drop your custom …") — never as an unlabelled trailing sentence.
65
+ - **Generated model names**: a new model template belonging to another model is nested under it, in `models/<owner>/` (`User::Session`, `User::PreviousPassword`), not prefixed (`UserSession`). Nested in an Active Record model, it keeps the prefixed table name (`user_sessions`) and its owner's `has_many` finds it without `class_name:`. The older `PagePath`, `PageVersion` and `TagCategory` move in Trek 3 (#248).
66
+ - **Generators' npm packages**: the install generators' `NPM_DEPENDENCIES` (`lib/generators/trek/install/*_generator.rb`) add to host apps the packages needed by the config files they copy from Trek's root (`esbuild.config.js`, `postcss.config.js`, `.eslintrc.json`, `.stylelintrc.json`, `lefthook.yml`). When a package is bumped in Trek's `package.json` — by hand or by Renovate, which doesn't touch the generators — bump its range in the generator in the same MR, or new apps get a version the copied config may not work with (as esbuild 0.15 did). List only packages those configs or the generated scripts use.
64
67
  - **Public code**: Trek ships publicly (gem, npm, docs), so nothing committed may mention a client or project name, nor a client's domain models or business logic — not in code, comments, specs, templates, docs, CHANGELOG or commit messages. Use neutral examples (`Page`, `Shop::Product`, an engine's `Blog::Post`). Client context belongs in GitLab issues and MR descriptions only.
65
68
  - **CI**: GitLab CI (`.gitlab-ci.yml`), stages build → lint → security → test → deploy; docs deploy to Netlify; deps via Renovate
data/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.4.0] - 2026-10-16
4
+
5
+ ### Added
6
+
7
+ - `Trek::Form::ImageFieldComponent` (`f.image_field`) shows a « See original » button opening the uploaded image in a new tab.
8
+ - An audit log: `rails g trek:install:audit_log` installs an `AuditLog` model recording each mutation in the admin (author, request, target record, status), sign-ins included, and an admin list for the `admin` role; `Trek::AuditLog::PurgeJob` deletes rows past 6 months. See [Audit log](https://trek.etamin.studio/reference/generators/audit-log). **Upgrade note:** an app with its own audit log can drop its copy for Trek's, keeping its `audit_logs` table (see the docs' migration steps).
9
+ - `Trek::Filters` exposes `filter_params` to views, as `Trek::Scopes` does `scope_params`.
10
+ - Users can't reuse one of their last 5 passwords (`Trek::Users::PasswordHistory`, configurable with `password_history_size`). **Upgrade note:** copy `app/models/user/previous_password.rb` (`User::PreviousPassword`) and the `user_previous_passwords` migration from Trek's templates (`lib/generators/trek/templates/{models,migrations}`), run it, add `include Trek::Users::PasswordHistory` after `has_secure_password` in `app/models/user.rb`, and re-run `rails g trek:install:model_specs` for the new specs.
11
+ - Password expiry and renewal (`Trek::Users::PasswordRenewal`): an admin can ask every user for a new password after a suspected compromise, and `password_expires_after` (none by default) makes passwords expire. A user concerned is signed out and sent to the new password form, which states the rules and rates the password as it is typed (`Trek::Form::PasswordStrengthComponent`). **Upgrade note:** copy the `user_password_renewal` migration from Trek's templates (`lib/generators/trek/templates/migrations`) and run it, add `include Trek::Users::PasswordRenewal` after `has_secure_password` in `app/models/user.rb`, and take the changes to `require_authentication` in `app/controllers/concerns/trek/authentication.rb`, the `require_password_renewal` action of `Admin::UsersController`, its route (`post :require_password_renewal, on: :collection` in `resources :users`), and the `admin/users/index` and `admin/user_password_resets/edit` views from Trek's templates.
12
+ - Sessions expire after 48 hours without a request and 30 days after signing in (`Trek::Users::SessionLifetime`, configurable on `User::Session`), and users see their active sessions in a tab of their profile, to sign out any other or all of them; an IP address shows its host name on hover. Session activity is written at most every 5 minutes instead of on every request. **Upgrade note:** copy the `user_session_tracking` migration from Trek's templates (`lib/generators/trek/templates/migrations`) and run it; add `include Trek::Users::SessionLifetime` to `app/models/user/session.rb` (or `app/models/user_session.rb`); take Trek's changes to `app/controllers/concerns/trek/authentication.rb` (`find_session_by_cookie`, `start_new_session_for`, `track_last_activity`), to `Admin::UsersController#show` and to `app/views/admin/users/show.html.slim` (with its new `_properties` and `_sessions` partials); copy `app/controllers/admin/active_sessions_controller.rb` from Trek's templates and add its route (`resources :active_sessions, only: :destroy do delete :others, action: :destroy_others, on: :collection end` in the admin namespace); schedule `Trek::PurgeExpiredSessionsJob` daily.
13
+ - Users see their previous sign-in (date, browser, IP address) on their profile's Active sessions tab, and a sign-in from a browser that never signed in as them emails them (`Trek::Users::SignIns`, `new_sign_in_email`). **Upgrade note:** copy the `user_sign_ins` migration and the `admin_mailer/new_sign_in` views from Trek's templates and run the migration; add `include Trek::Users::SignIns` to `app/models/user.rb`; take Trek's changes to `app/controllers/concerns/trek/authentication.rb` (`record_sign_in_of` and the methods after it), `app/controllers/admin/user_sessions_controller.rb` (`create`, `sign_in_notice`), `app/mailers/admin_mailer.rb` (`new_sign_in`) and `app/views/admin/users/_sessions.html.slim`.
14
+ - `Trek::SectionComponent`: a titled section of a page (an `h2` labelling it), with an optional hint under the heading.
15
+ - `NotDisposableValidator` refuses email addresses from disposable providers, on any model: `validates :email, not_disposable: true`. The domains come from the `valid_email2` gem, which keeps them up to date. See [Validators](https://trek.etamin.studio/reference/validators).
16
+ - Failed sign-ins are throttled per account and per IP address over a longer window: after 5 failures in a row, an account's sign-in locks for 1 minute, doubled on each further failure up to 1 hour, and its user is emailed (`Trek::Users::SignInLockout`); an IP address gets 20 failures an hour, whatever the account. A locked account gets the same answer as a wrong password, and a failed or throttled sign-in now renders the form (422, 429) instead of redirecting, so the audit log counts it as a failure, on the account tried. See [Failed sign-ins](https://trek.etamin.studio/reference/models/user#failed-sign-ins), including why `Rails.cache` must be shared between servers. **Upgrade note:** copy the `user_sign_in_lockout` migration and the `admin_mailer/sign_in_locked` views from Trek's templates and run the migration; add `include Trek::Users::SignInLockout` to `app/models/user.rb`; take Trek's `app/controllers/admin/user_sessions_controller.rb` and the `sign_in_locked` method of `app/mailers/admin_mailer.rb`.
17
+ - Passkeys: users add and delete passkeys on a new tab of their profile, which names each after its password manager, and sign in with one instead of their password, from a button on the sign-in form or the email field's autofill (`Trek::Users::Passkeys`, `User::Passkey`, through the `webauthn` gem). The password keeps working. See [Passkeys](https://trek.etamin.studio/reference/models/user#passkeys). **Upgrade note:** copy `app/models/user/passkey.rb` and the `user_passkeys` migration from Trek's templates and run it; add `include Trek::Users::Passkeys` to `app/models/user.rb`; copy `app/controllers/admin/passkeys_controller.rb` and `app/controllers/admin/passkey_sessions_controller.rb`, and add their routes in the admin namespace (`resources :passkeys, only: %i[create destroy] do get :options, on: :collection end` and `resource :passkey_session, only: :create do get :options end`); take Trek's changes to `app/views/admin/user_sessions/new.html.slim`, `app/views/admin/users/show.html.slim` (with the new `_passkeys` partial) and `Admin::UsersController#show`; with the audit log, add `Admin::PasskeySessionsController` to `config/initializers/audit_log.rb`.
18
+ - A [Security and compliance](https://trek.etamin.studio/security) docs page maps Trek's security measures to ISO 27001's controls and NIS2's measures, and lists what stays with the app, its hosting and the organization.
19
+ - The [Security and compliance](https://trek.etamin.studio/security#cryptography) page lists the cryptography a generated app uses, and how each algorithm stands against a quantum computer.
20
+ - The generated GitLab CI has an `sbom` job, commented out, making a Software Bill of Materials (CycloneDX, with cdxgen) of the app's gems and npm packages, for a client who asks for one. See [Software Bill of Materials](https://trek.etamin.studio/reference/generators/install#software-bill-of-materials). **Upgrade note:** when a client asks for an SBOM, copy the `sbom` job from Trek's `lib/generators/trek/templates/.gitlab-ci.yml` into the app's `.gitlab-ci.yml`, uncommented.
21
+ - `ADMIN_ALLOWED_IPS` restricts the admin to IP addresses and ranges (e.g. `203.0.113.0/24, 2001:db8::/32`): from anywhere else, `/admin` and everything below it answer like an unknown URL, a 404. Off when unset. Behind a CDN, list its ranges in `trusted_proxies` first. See [Restricting the admin to IP addresses](https://trek.etamin.studio/proxies#restricting-the-admin-to-ip-addresses).
22
+ - Security headers: `rails g trek:install:security_headers`, which `trek:install` runs, sets up a Content Security Policy, report-only at first, allowing no inline script, and a `Permissions-Policy` header turning off the browser features the app doesn't use (camera, microphone, geolocation…). Violations are reported to `/csp-reports` (`Trek::CspReportsController`), which sends them to AppSignal, one incident per directive and blocked origin, and to the log, throttled against floods. It replaces Rails' commented-out example, but leaves an app's own policy. HSTS, from `force_ssl`, already covers subdomains; its `preload` option is documented. See [Security headers](https://trek.etamin.studio/reference/generators/security-headers). **Upgrade note:** run `rails g trek:install:security_headers`, browse the app with the console open and watch AppSignal's `csp` namespace, add the front office's third-party hosts, then set `content_security_policy_report_only` to `false`.
23
+
24
+ ### Changed
25
+
26
+ - Trek pins **Node 24.22.0** (was 24.18.0) in `.node-version`. **Upgrade note:** existing apps can set `24.22.0` in their `.node-version`.
27
+ - `trek:install:yarn` installs **Yarn 4.18.1** (was 4.18.0), and Trek's own development moves to it from Yarn 3, with its defaults: no install scripts, and no package published less than a day ago. **Upgrade note:** existing apps can upgrade with `yarn set version 4.18.1` followed by `yarn install`.
28
+ - Passwords follow the ANSSI's recommendations for a privileged access (`Trek::Users::PasswordPolicy`, included by `Trek::Users::Validations`): at least 12 characters, with a digit and a symbol below 20, and neither a common password, a keyboard or number sequence, nor the user's name or email; a Have I Been Pwned check is opt-in. Existing passwords keep working: the rules apply when one is set. **Upgrade note:** the rules are configurable in `app/models/user.rb` (`password_min_length`, `passphrase_length`, `password_check_pwned`); update seeds and specs whose passwords no longer pass.
29
+ - The generated signed-in browser model is `User::Session` (`user.sessions`), in the same `user_sessions` table. Apps generated before keep working with `UserSession`. **Upgrade note:** to follow, move `app/models/user_session.rb` to `app/models/user/session.rb` as `class User::Session`, replace `has_many :user_sessions` with `has_many :sessions` in `app/models/user.rb`, and update `UserSession.find_by` and `user.user_sessions` in `app/controllers/concerns/trek/authentication.rb` and `app/controllers/admin/user_password_resets_controller.rb`; no migration is needed.
30
+ - The admin's Inter font comes with Trek's npm package ([`@fontsource/inter`](https://fontsource.org/fonts/inter)), bundled into `admin.css`, instead of Google Fonts: visitors' browsers no longer contact Google, and the Content Security Policy needs no Google host. **Upgrade note:** remove `admin_webfonts_link_tag` from `app/helpers/application_helper.rb` and `app/views/layouts/admin.html.slim`, and the Google Fonts `link` tags from both admin layouts (`admin.html.slim`, `admin/auth.html.slim`); `esbuild.config.js` must have the `.woff` and `.woff2` file loaders, as Trek's has.
31
+
32
+ ### Deprecated
33
+
34
+ - `Trek::UserSession`, the sign-in form object Trek 2 no longer uses, warns and will be removed in Trek 3. **Upgrade note:** in a sign-in controller still building one, pass the params to `User.authenticate_by(email:, password:)` instead, as Trek's generated `Admin::UserSessionsController` does.
35
+
36
+ ### Changed
37
+
38
+ - The install generators no longer add `esbuild-plugin-import-glob`, `postcss-cli`, `postcss-nesting` and `postcss-url`, which no generated config uses. **Upgrade note:** you can drop them from the app's `package.json` (`yarn remove …`), except those its own `postcss.config.js`, `esbuild.config.js` or `package.json` scripts still use (e.g. `postcss-url` to rebase the URLs of `@import`-ed CSS).
39
+ - `rails g trek:install` runs Trek's own generators in its process instead of starting `rails generate` for each, which saves about two minutes.
40
+
41
+ ### Fixed
42
+
43
+ - The generated `.gitlab-ci.yml`'s commented-out deploy jobs point at `example.com` instead of a real site.
44
+ - A tag's key is unique within its category, as `Tag` validates it and the docs said: the generated `tags` migration's unique index on `key` alone refused the same key in two categories with `ActiveRecord::RecordNotUnique`. A tag category with tags can no longer be deleted, which left them without a category, and the admin now says why a deletion is refused instead of reporting it done. **Upgrade note:** in `app/models/tag_category.rb`, replace `dependent: :nullify` with `dependent: :restrict_with_error`. Check that no category has two tags with the same key, nor two tags without a category, then generate a migration (`bin/rails g migration TagKeyUniquePerCategory`) with `remove_index :tags, name: "index_tags_on_key", if_exists: true` (apps generated with Trek 0.1.9 or later have it), `add_index :tags, %i[category_id key], unique: true` and `add_index :tags, :key, unique: true, where: "category_id IS NULL", name: "index_tags_on_key_without_category"`. Copy `spec/models/tag_spec.rb` from Trek's templates for the specs.
45
+ - The notice after signing in is in the user's language, rather than the app's default one.
46
+ - The password reset request no longer reveals whether an email has an account: it answers every email the same way, and queues `Admin::PasswordResetJob` for each, which emails privileged users only, so the response takes as long either way. **Upgrade note:** copy `app/jobs/admin/password_reset_job.rb` from Trek's templates (`lib/generators/trek/templates/jobs/admin`), and in `Admin::UserPasswordResetsController#create`, replace the user lookup and `deliver_later` with `Admin::PasswordResetJob.perform_later(params[:email].to_s)`.
47
+ - Icons drawn in black (`stroke="#000000"`, such as the content editor's toolbar icons) stayed black in the dark theme, and in any colored context: since the sprite was sanitized in two passes, black was turned into `currentColor` before the icon's paths were inserted, so it never reached them. The substitution now runs once the paths are in.
48
+ - `trek:install:esbuild` installed esbuild 0.15, too old for the `esbuild.config.js` it copies: `yarn build --watch` (and `bin/dev`) crashed with `esbuild.context is not a function`. It now installs esbuild 0.28, as Trek does. **Upgrade note:** in an app generated by an earlier Trek 2, run `yarn add --dev esbuild@^0.28.0`.
49
+ - The generated invitation and password reset emails formatted the link's expiry (« 7 days ») in the app's default locale instead of the recipient's. `rails g trek:install:model_specs` now also installs mailer specs covering it. **Upgrade note:** in `app/mailers/admin_mailer.rb` and `app/mailers/admin/passwords_mailer.rb`, move the `@expiration_delay = …` line inside the `I18n.with_locale` block, before `mail`; re-run `rails g trek:install:model_specs` for the new specs.
50
+
51
+ ## [2.3.2] - 2026-10-02
52
+
53
+ ### Fixed
54
+
55
+ - **Security:** `Trek::Scopes#apply_scopes` called any name found in `params[:scopes]` that the relation answered to, not only the controller's `available_scopes`, which let a request run unintended methods on the listed records. Only the names in `available_scopes` are applied now; others are ignored. **Upgrade note:** update Trek as soon as possible. A controller passing `apply_scopes` a scope name it does not list in `available_scopes` must add it there.
56
+ - The generated admin sign-in, password reset request and new password pages rendered their actions with the default aspect of `Trek::Form::ActionsComponent`, whose sticky action bar (top margin, padding, background) is meant for edit forms: inside those small cards, the button and the « forgotten password » link sat in a padded bar. They now use the `seamless` aspect. **Upgrade note:** in apps generated before this version, pass `aspect: :seamless` to `Trek::Form::ActionsComponent.new` in `app/views/admin/user_sessions/new.html.slim` and `app/views/admin/user_password_resets/{new,edit}.html.slim`.
57
+
3
58
  ## [2.3.1] - 2026-10-02
4
59
 
5
60
  ### Changed
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- trek (2.3.1)
4
+ trek (2.4.0)
5
5
  action_policy (~> 0.6)
6
6
  actioncable
7
7
  acts_as_list (~> 1.1)
@@ -30,9 +30,12 @@ PATH
30
30
  shrine-blurhash (~> 0.2)
31
31
  slim-rails (~> 4.0)
32
32
  strip_attributes (~> 2.0)
33
+ time_for_a_boolean (~> 0.3)
33
34
  turbo-rails (>= 1.0)
35
+ valid_email2 (~> 7.1)
34
36
  view_component (< 5.0)
35
37
  view_component-form (~> 0.3)
38
+ webauthn (~> 3.4)
36
39
  zeitwerk (~> 2.6)
37
40
 
38
41
  GEM
@@ -119,6 +122,7 @@ GEM
119
122
  activesupport (>= 6.1)
120
123
  addressable (2.9.0)
121
124
  public_suffix (>= 2.0.2, < 8.0)
125
+ android_key_attestation (0.3.0)
122
126
  ast (2.4.3)
123
127
  aws-eventstream (1.4.0)
124
128
  aws-partitions (1.1200.0)
@@ -142,6 +146,7 @@ GEM
142
146
  base64 (0.3.0)
143
147
  bcrypt (3.1.22)
144
148
  bigdecimal (4.1.2)
149
+ bindata (2.5.1)
145
150
  blurhash (0.1.8)
146
151
  brakeman (7.1.2)
147
152
  racc
@@ -158,6 +163,7 @@ GEM
158
163
  rack-test (>= 0.6.3)
159
164
  regexp_parser (>= 1.5, < 3.0)
160
165
  xpath (~> 3.2)
166
+ cbor (0.5.10.3)
161
167
  closure_tree (9.3.0)
162
168
  activerecord (>= 7.2.0)
163
169
  with_advisory_lock (>= 7.0.0)
@@ -165,6 +171,9 @@ GEM
165
171
  concurrent-ruby (1.3.8)
166
172
  connection_pool (3.0.2)
167
173
  content_disposition (1.0.0)
174
+ cose (1.3.1)
175
+ cbor (~> 0.5.9)
176
+ openssl-signature_algorithm (~> 1.0)
168
177
  crass (1.0.7)
169
178
  css_parser (3.0.0)
170
179
  addressable
@@ -245,6 +254,8 @@ GEM
245
254
  reline (>= 0.4.2)
246
255
  jmespath (1.6.2)
247
256
  json (2.21.2)
257
+ jwt (3.3.0)
258
+ base64
248
259
  kaminari (1.2.2)
249
260
  activesupport (>= 4.1.0)
250
261
  kaminari-actionview (= 1.2.2)
@@ -310,6 +321,9 @@ GEM
310
321
  nokogiri (1.19.4-x86_64-darwin)
311
322
  racc (~> 1.4)
312
323
  oaken (1.0.0)
324
+ openssl (4.0.2)
325
+ openssl-signature_algorithm (1.3.0)
326
+ openssl (> 2.0)
313
327
  parallel (1.27.0)
314
328
  parser (3.3.10.0)
315
329
  ast (~> 2.4.1)
@@ -466,6 +480,8 @@ GEM
466
480
  ruby-vips (2.3.0)
467
481
  ffi (~> 1.12)
468
482
  logger
483
+ safety_net_attestation (0.5.0)
484
+ jwt (>= 2.0, < 4.0)
469
485
  securerandom (0.4.1)
470
486
  shrine (3.10.0)
471
487
  content_disposition (~> 1.0)
@@ -502,7 +518,14 @@ GEM
502
518
  unicode-display_width (>= 1.1.1, < 4)
503
519
  thor (1.5.0)
504
520
  tilt (2.6.1)
521
+ time_for_a_boolean (0.3.0)
522
+ activerecord
523
+ railties
505
524
  timeout (0.6.1)
525
+ tpm-key_attestation (0.14.2)
526
+ bindata (~> 2.4)
527
+ openssl (> 2.0)
528
+ openssl-signature_algorithm (~> 1.0)
506
529
  tsort (0.2.0)
507
530
  turbo-rails (2.0.23)
508
531
  actionpack (>= 7.1.0)
@@ -514,6 +537,9 @@ GEM
514
537
  unicode-emoji (4.2.0)
515
538
  uri (1.1.1)
516
539
  useragent (0.16.11)
540
+ valid_email2 (7.1.0)
541
+ activemodel (>= 6.0)
542
+ mail (~> 2.5)
517
543
  view_component (4.12.0)
518
544
  actionview (>= 7.1.0)
519
545
  activesupport (>= 7.1.0)
@@ -523,6 +549,14 @@ GEM
523
549
  activesupport (>= 7.2.0, < 8.2)
524
550
  view_component (>= 2.34.0, < 5.0)
525
551
  zeitwerk (~> 2.5)
552
+ webauthn (3.4.3)
553
+ android_key_attestation (~> 0.3.0)
554
+ bindata (~> 2.4)
555
+ cbor (~> 0.5.9)
556
+ cose (~> 1.1)
557
+ openssl (>= 2.2)
558
+ safety_net_attestation (~> 0.5.0)
559
+ tpm-key_attestation (~> 0.14.0)
526
560
  websocket-driver (0.8.2)
527
561
  base64
528
562
  websocket-extensions (>= 0.1.0)
data/Makefile CHANGED
@@ -6,12 +6,24 @@ lint:
6
6
  yarn eslint "app/**/*.js"
7
7
  yarn stylelint "app/**/*.css"
8
8
  yarn prettier --check "{app,config,lib,spec}/**/*.{css,js,json,md,yml}"
9
+ bundle exec i18n-tasks missing -t diff
9
10
 
10
11
  fix:
11
12
  bundle exec rubocop --autocorrect
12
13
  yarn eslint "app/**/*.js" --fix
13
14
  yarn stylelint "app/**/*.css" --fix
14
15
  yarn prettier --write "{app,config,lib,spec}/**/*.{css,js,json,md,yml}"
16
+ $(MAKE) i18n-normalize
17
+
18
+ # Sort the locale keys (i18n-tasks), then format the files as the rest of the repo (Prettier): each
19
+ # tool alone undoes the other's style, so they always run together.
20
+ i18n-normalize:
21
+ bundle exec i18n-tasks normalize
22
+ yarn prettier --write "config/locales/*.yml"
23
+
24
+ # Fails when the locale files aren't normalized (see i18n-normalize)
25
+ i18n-check: i18n-normalize
26
+ git diff --exit-code config/locales
15
27
 
16
28
  release:
17
29
  @read -p "New version (current: $$(sed -n 's/.*VERSION = "\(.*\)"/\1/p' lib/trek/version.rb)): " version; \
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
2
+ <path fill-rule="evenodd" d="M4.8,4A4,4,0,1,1,.8,8,4,4,0,0,1,4.8,4Zm0,2.4A1.6,1.6,0,1,0,6.4,8,1.6,1.6,0,0,0,4.8,6.4Z"/>
3
+ <path d="M15.2,8.8H8.4V7.2h6.8a.8.8,0,0,1,0,1.6Z"/>
4
+ <path d="M12.8,11.2a.8.8,0,0,1-.8-.8V8h1.6v2.4A.8.8,0,0,1,12.8,11.2Z"/>
5
+ </svg>
@@ -41,6 +41,12 @@ body {
41
41
  transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
42
42
  }
43
43
 
44
+ /* A term with more to say on hover, e.g. an IP address and its host name */
45
+ abbr[title] {
46
+ cursor: help;
47
+ text-decoration: underline dotted;
48
+ }
49
+
44
50
  a {
45
51
  color: var(--accent-9);
46
52
  transition: color 0.2s ease;
@@ -119,3 +119,9 @@ table .root {
119
119
  content: "•";
120
120
  margin: 0 1rem;
121
121
  }
122
+
123
+ /* A status or a button: it keeps its width, and the content wraps instead. */
124
+ .status {
125
+ flex-shrink: 0;
126
+ white-space: nowrap;
127
+ }
@@ -31,3 +31,12 @@
31
31
  size: :narrow,
32
32
  data: remove_button_data_attributes,
33
33
  )
34
+ / The uploaded file itself, the preview being a thumbnail
35
+ = render Trek::ButtonComponent.new( \
36
+ text: t("admin.actions.see_original"),
37
+ size: :narrow,
38
+ href: image.url,
39
+ target: "_blank",
40
+ rel: "noopener",
41
+ ) do |button|
42
+ - button.with_icon("trek/external")
@@ -0,0 +1,56 @@
1
+ /* Use component-local class names and add them to HTML via #class_for(name) helper */
2
+
3
+ /* The controller sets data-level, from 0 (too short) to 3 (strong), once a password is typed. */
4
+
5
+ .strength {
6
+ align-items: center;
7
+ display: flex;
8
+ gap: 1.2rem;
9
+ margin-top: 0.8rem;
10
+ }
11
+
12
+ .bar {
13
+ display: flex;
14
+ flex: 1;
15
+ gap: 0.4rem;
16
+ }
17
+
18
+ .segment {
19
+ background-color: var(--slate-5);
20
+ border-radius: 0.2rem;
21
+ flex: 1;
22
+ height: 0.4rem;
23
+ transition: background-color 0.15s ease-out;
24
+ }
25
+
26
+ .label {
27
+ color: var(--slate-11);
28
+ font-size: 0.8em;
29
+ min-width: 8ch;
30
+ text-align: right;
31
+ }
32
+
33
+ .root[data-level="0"] .label,
34
+ .root[data-level="1"] .label {
35
+ color: var(--tomato-11);
36
+ }
37
+
38
+ .root[data-level="1"] .segment:nth-child(-n + 1) {
39
+ background-color: var(--tomato-9);
40
+ }
41
+
42
+ .root[data-level="2"] .label {
43
+ color: var(--amber-11);
44
+ }
45
+
46
+ .root[data-level="2"] .segment:nth-child(-n + 2) {
47
+ background-color: var(--amber-9);
48
+ }
49
+
50
+ .root[data-level="3"] .label {
51
+ color: var(--grass-11);
52
+ }
53
+
54
+ .root[data-level="3"] .segment {
55
+ background-color: var(--grass-9);
56
+ }
@@ -0,0 +1,8 @@
1
+ div(class=root_class data=data_attributes)
2
+ = content
3
+ div class=class_for("strength")
4
+ div class=class_for("bar") aria-hidden="true"
5
+ - (LEVELS.size - 1).times do
6
+ span class=class_for("segment")
7
+ span(class=class_for("label") aria-live="polite"
8
+ data=stimulus_target_hash(:label))
@@ -0,0 +1,74 @@
1
+ import "./password_strength_component.css";
2
+
3
+ import { Controller as BaseController } from "@hotwired/stimulus";
4
+
5
+ // Keyboard rows (AZERTY and QWERTY), the alphabet and the digits, as Trek::Users::PasswordPolicy
6
+ const SEQUENCES = [
7
+ "azertyuiop",
8
+ "qsdfghjklm",
9
+ "wxcvbn",
10
+ "qwertyuiop",
11
+ "asdfghjkl",
12
+ "zxcvbnm",
13
+ "abcdefghijklmnopqrstuvwxyz",
14
+ "01234567890",
15
+ ];
16
+ const SEQUENCE_LENGTH = 5;
17
+
18
+ // Rates the password typed from 0 (too short) to 3 (strong). Only a guide: the server checks the
19
+ // common passwords and the user's name and email too.
20
+ export class Controller extends BaseController {
21
+ static targets = ["label"];
22
+ static values = {
23
+ minLength: Number,
24
+ passphraseLength: Number,
25
+ labels: Array,
26
+ };
27
+
28
+ connect() {
29
+ this.input = this.element.querySelector("input");
30
+ this.update = this.update.bind(this);
31
+ this.input.addEventListener("input", this.update);
32
+ this.update();
33
+ }
34
+
35
+ disconnect() {
36
+ this.input.removeEventListener("input", this.update);
37
+ }
38
+
39
+ update() {
40
+ const password = this.input.value;
41
+ const level = password ? this.level(password) : 0;
42
+
43
+ // Read by the CSS, which colors the bar and the label
44
+ if (password) this.element.dataset.level = level;
45
+ else delete this.element.dataset.level;
46
+ this.labelTarget.textContent = password ? this.labelsValue[level] : "";
47
+ }
48
+
49
+ level(password) {
50
+ if (password.length < this.minLengthValue) return 0;
51
+ if (this.sequence(password)) return 1;
52
+ if (password.length >= this.passphraseLengthValue) return 3;
53
+
54
+ const complex = /\d/.test(password) && /[^\p{L}\p{N}\s]/u.test(password);
55
+ if (!complex) return 1;
56
+
57
+ return password.length >= this.minLengthValue + 4 ? 3 : 2;
58
+ }
59
+
60
+ sequence(password) {
61
+ const normalized = password.toLowerCase().replace(/[^a-z0-9]/g, "");
62
+ if (new RegExp(`(.)\\1{${SEQUENCE_LENGTH - 1}}`).test(normalized))
63
+ return true;
64
+
65
+ return SEQUENCES.some((row) => {
66
+ const reversed = [...row].reverse().join("");
67
+ for (let i = 0; i + SEQUENCE_LENGTH <= normalized.length; i++) {
68
+ const chunk = normalized.slice(i, i + SEQUENCE_LENGTH);
69
+ if (row.includes(chunk) || reversed.includes(chunk)) return true;
70
+ }
71
+ return false;
72
+ });
73
+ }
74
+ }
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trek
4
+ module Form
5
+ # Wraps a new password field with a bar rating the password as it is typed, against the rules
6
+ # of Trek::Users::PasswordPolicy. Only a guide: the model has the last word.
7
+ #
8
+ # = render Trek::Form::PasswordStrengthComponent.new(min_length: User.password_min_length,
9
+ # passphrase_length: User.passphrase_length) do
10
+ # = f.password_field :password, autocomplete: "new-password"
11
+ class PasswordStrengthComponent < Trek::Component
12
+ LEVELS = %i[too_short weak fair strong].freeze
13
+
14
+ option :min_length
15
+ option :passphrase_length
16
+
17
+ private
18
+
19
+ def data_attributes
20
+ {
21
+ controller: identifier,
22
+ "#{identifier}-min-length-value": min_length,
23
+ "#{identifier}-passphrase-length-value": passphrase_length,
24
+ "#{identifier}-labels-value": LEVELS.map { |level| t("trek.password_strength.#{level}") }.to_json
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -21,3 +21,10 @@
21
21
  margin-bottom: 3.2rem;
22
22
  padding-bottom: 2.4rem;
23
23
  }
24
+
25
+ /* Another way in below the first form, e.g. signing in with a passkey */
26
+ .wrapper > form + form {
27
+ border-top: 1px solid var(--slate-6);
28
+ margin-top: 3.2rem;
29
+ padding-top: 3.2rem;
30
+ }
@@ -67,11 +67,12 @@ module Trek
67
67
  # Extract inner content (children) from the second pass
68
68
  inner_content = children_sanitized.sub(/<svg[^>]*>/, "").sub("</svg>", "")
69
69
 
70
- # Combine: symbol element from first pass + children from second pass
70
+ # Combine: symbol element from first pass + children from second pass. Black becomes
71
+ # currentColor once the children are in: they carry the strokes and fills.
71
72
  svg_sanitized
72
73
  .sub("<svg", "<symbol id=\"c-icon-#{key}\"")
73
- .gsub(/#0{3,6}/, "currentColor")
74
74
  .sub(">", ">#{inner_content}")
75
+ .gsub(/#0{3,6}/, "currentColor")
75
76
  .sub("</svg>", "</symbol>")
76
77
  .gsub(/[\r\n]+/, "")
77
78
  .strip
@@ -0,0 +1,22 @@
1
+ /* Use component-local class names and add them to HTML via #class_for(name) helper */
2
+
3
+ .root {
4
+ line-height: 1.25;
5
+ margin-bottom: 3.2rem;
6
+ max-width: 64rem;
7
+ }
8
+
9
+ /* As a property's title */
10
+ .title {
11
+ color: var(--slate-12);
12
+ font-size: inherit;
13
+ font-weight: var(--semibold);
14
+ margin: 0 0 1.2rem;
15
+ }
16
+
17
+ /* As a form field's hint */
18
+ .hint {
19
+ color: var(--slate-11);
20
+ font-size: 0.8em;
21
+ margin: -0.6rem 0 1.2rem;
22
+ }
@@ -0,0 +1,5 @@
1
+ section(class=root_class aria-labelledby=heading_id)
2
+ h2(id=heading_id class=class_for("title")) = title
3
+ - if hint
4
+ p(class=class_for("hint")) = hint
5
+ = content
@@ -0,0 +1 @@
1
+ import "./section_component.css";
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trek
4
+ # A titled section of a page (whose own title is the h1), e.g. a list under its heading, with an
5
+ # optional hint between them
6
+ class SectionComponent < Trek::Component
7
+ option :title
8
+ option :hint, optional: true
9
+
10
+ def heading_id
11
+ @heading_id ||= "section-#{SecureRandom.hex(4)}"
12
+ end
13
+ end
14
+ end