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.
- checksums.yaml +4 -4
- data/.node-version +1 -1
- data/.yarn/releases/yarn-4.18.1.cjs +1000 -0
- data/.yarnrc.yml +3 -5
- data/AGENTS.md +7 -4
- data/CHANGELOG.md +55 -0
- data/Gemfile.lock +35 -1
- data/Makefile +12 -0
- data/app/assets/icons/trek/passkey.svg +5 -0
- data/app/assets/stylesheets/trek/base.css +6 -0
- data/app/components/trek/entries/item_component/item_component.css +6 -0
- data/app/components/trek/form/image_field_component/image_field_component.html.slim +9 -0
- data/app/components/trek/form/password_strength_component/password_strength_component.css +56 -0
- data/app/components/trek/form/password_strength_component/password_strength_component.html.slim +8 -0
- data/app/components/trek/form/password_strength_component/password_strength_component.js +74 -0
- data/app/components/trek/form/password_strength_component.rb +29 -0
- data/app/components/trek/gate_component/gate_component.css +7 -0
- data/app/components/trek/icon/sprite_component.rb +3 -2
- data/app/components/trek/section_component/section_component.css +22 -0
- data/app/components/trek/section_component/section_component.html.slim +5 -0
- data/app/components/trek/section_component/section_component.js +1 -0
- data/app/components/trek/section_component.rb +14 -0
- data/app/controllers/concerns/trek/auditable.rb +103 -0
- data/app/controllers/concerns/trek/filters.rb +2 -0
- data/app/controllers/concerns/trek/passkeys.rb +37 -0
- data/app/controllers/concerns/trek/scopes.rb +9 -2
- data/app/controllers/trek/csp_reports_controller.rb +62 -0
- data/app/controllers/trek/resource_controller.rb +7 -1
- data/app/helpers/trek/audit_logs_helper.rb +21 -0
- data/app/helpers/trek/user_agents_helper.rb +36 -0
- data/app/javascript/controllers/index.js +3 -0
- data/app/javascript/controllers/passkey_controller.js +118 -0
- data/app/javascript/trek.js +4 -0
- data/app/jobs/trek/audit_log/purge_job.rb +18 -0
- data/app/jobs/trek/purge_expired_sessions_job.rb +14 -0
- data/app/jobs/trek/resolve_session_hostname_job.rb +17 -0
- data/app/models/concerns/trek/audit_log.rb +30 -0
- data/app/models/concerns/trek/users/passkeys.rb +75 -0
- data/app/models/concerns/trek/users/password_history.rb +51 -0
- data/app/models/concerns/trek/users/password_policy.rb +85 -0
- data/app/models/concerns/trek/users/password_renewal.rb +51 -0
- data/app/models/concerns/trek/users/session_lifetime.rb +39 -0
- data/app/models/concerns/trek/users/sign_in_lockout.rb +67 -0
- data/app/models/concerns/trek/users/sign_ins.rb +31 -0
- data/app/models/concerns/trek/users/validations.rb +3 -2
- data/app/models/trek/feature.rb +6 -0
- data/app/models/trek/pwned_passwords.rb +40 -0
- data/app/models/trek/user_session.rb +10 -0
- data/app/policies/trek/audit_log_policy.rb +34 -0
- data/app/policies/trek/user_policy.rb +5 -0
- data/app/validators/not_disposable_validator.rb +18 -0
- data/config/brakeman.ignore +22 -0
- data/config/i18n-tasks.yml +7 -0
- data/config/locales/trek.en.yml +144 -39
- data/config/locales/trek.fr.yml +145 -40
- data/docs/.vitepress/config.mjs +8 -3
- data/docs/deploy.md +2 -0
- data/docs/proxies.md +53 -0
- data/docs/reference/components.md +1 -0
- data/docs/reference/controllers.md +1 -1
- data/docs/reference/environment-variables.md +2 -0
- data/docs/reference/generators/admin-user.md +1 -1
- data/docs/reference/generators/audit-log.md +113 -0
- data/docs/reference/generators/install.md +12 -3
- data/docs/reference/generators/security-headers.md +109 -0
- data/docs/reference/index.md +3 -0
- data/docs/reference/models/tag.md +4 -2
- data/docs/reference/models/user.md +123 -3
- data/docs/reference/validators.md +94 -0
- data/docs/security.md +90 -0
- data/lefthook.yml +8 -0
- data/lib/generators/trek/install/admin/user_password_resets_generator.rb +5 -0
- data/lib/generators/trek/install/admin/user_sessions_generator.rb +5 -0
- data/lib/generators/trek/install/admin/users_generator.rb +23 -0
- data/lib/generators/trek/install/audit_log_generator.rb +56 -0
- data/lib/generators/trek/install/authentication_generator.rb +77 -3
- data/lib/generators/trek/install/esbuild_generator.rb +1 -2
- data/lib/generators/trek/install/mailer_generator.rb +18 -0
- data/lib/generators/trek/install/model_specs_generator.rb +6 -4
- data/lib/generators/trek/install/postcss_generator.rb +1 -4
- data/lib/generators/trek/install/security_headers_generator.rb +49 -0
- data/lib/generators/trek/install/yarn_generator.rb +1 -1
- data/lib/generators/trek/install_generator.rb +62 -54
- data/lib/generators/trek/taxonomies_generator.rb +5 -0
- data/lib/generators/trek/templates/.gitlab-ci.yml +18 -2
- data/lib/generators/trek/templates/controllers/admin/active_sessions_controller.rb +21 -0
- data/lib/generators/trek/templates/controllers/admin/audit_logs_controller.rb +30 -0
- data/lib/generators/trek/templates/controllers/admin/passkey_sessions_controller.rb +41 -0
- data/lib/generators/trek/templates/controllers/admin/passkeys_controller.rb +37 -0
- data/lib/generators/trek/templates/controllers/admin/user_password_resets_controller.rb +4 -4
- data/lib/generators/trek/templates/controllers/admin/user_sessions_controller.rb +53 -4
- data/lib/generators/trek/templates/controllers/admin/users_controller.rb +20 -0
- data/lib/generators/trek/templates/controllers/concerns/trek/authentication.rb +58 -7
- data/lib/generators/trek/templates/helpers/application_helper.rb +0 -4
- data/lib/generators/trek/templates/initializers/audit_log.rb +11 -0
- data/lib/generators/trek/templates/initializers/content_security_policy.rb +49 -0
- data/lib/generators/trek/templates/initializers/permissions_policy.rb +19 -0
- data/lib/generators/trek/templates/jobs/admin/password_reset_job.rb +9 -0
- data/lib/generators/trek/templates/layouts/admin/auth.html.slim +0 -5
- data/lib/generators/trek/templates/layouts/admin.html.slim +0 -3
- data/lib/generators/trek/templates/mailers/admin/passwords_mailer.rb +1 -1
- data/lib/generators/trek/templates/mailers/admin_mailer.rb +28 -1
- data/lib/generators/trek/templates/migrations/audit_logs.rb.erb +27 -0
- data/lib/generators/trek/templates/migrations/tags.rb.erb +6 -1
- data/lib/generators/trek/templates/migrations/user_passkeys.rb.erb +24 -0
- data/lib/generators/trek/templates/migrations/user_password_renewal.rb.erb +8 -0
- data/lib/generators/trek/templates/migrations/user_previous_passwords.rb.erb +10 -0
- data/lib/generators/trek/templates/migrations/user_session_tracking.rb.erb +11 -0
- data/lib/generators/trek/templates/migrations/user_sign_in_lockout.rb.erb +7 -0
- data/lib/generators/trek/templates/migrations/user_sign_ins.rb.erb +13 -0
- data/lib/generators/trek/templates/models/audit_log.rb +3 -0
- data/lib/generators/trek/templates/models/tag_category.rb +2 -1
- data/lib/generators/trek/templates/models/user/passkey.rb +17 -0
- data/lib/generators/trek/templates/models/user/previous_password.rb +4 -0
- data/lib/generators/trek/templates/models/user/session.rb +8 -0
- data/lib/generators/trek/templates/policies/audit_log.rb +4 -0
- data/lib/generators/trek/templates/spec/mailers/admin/passwords_mailer_spec.rb +16 -0
- data/lib/generators/trek/templates/spec/mailers/admin_mailer_spec.rb +50 -0
- data/lib/generators/trek/templates/spec/models/audit_log_spec.rb +43 -0
- data/lib/generators/trek/templates/spec/models/tag_spec.rb +52 -0
- data/lib/generators/trek/templates/spec/models/user/session_spec.rb +68 -0
- data/lib/generators/trek/templates/spec/models/user_spec.rb +142 -4
- data/lib/generators/trek/templates/spec/requests/admin/active_sessions_spec.rb +119 -0
- data/lib/generators/trek/templates/spec/requests/admin/audit_logs_spec.rb +149 -0
- data/lib/generators/trek/templates/spec/requests/admin/passkey_sessions_spec.rb +103 -0
- data/lib/generators/trek/templates/spec/requests/admin/passkeys_spec.rb +104 -0
- data/lib/generators/trek/templates/spec/requests/admin/password_renewal_spec.rb +83 -0
- data/lib/generators/trek/templates/spec/requests/admin/sign_in_lockout_spec.rb +95 -0
- data/lib/generators/trek/templates/spec/requests/admin/sign_in_notices_spec.rb +93 -0
- data/lib/generators/trek/templates/spec/requests/admin/user_password_resets_spec.rb +55 -6
- data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +31 -2
- data/lib/generators/trek/templates/views/admin/audit_logs/index.html.slim +35 -0
- data/lib/generators/trek/templates/views/admin/audit_logs/show.html.slim +50 -0
- data/lib/generators/trek/templates/views/admin/user_password_resets/edit.html.slim +17 -7
- data/lib/generators/trek/templates/views/admin/user_password_resets/new.html.slim +3 -2
- data/lib/generators/trek/templates/views/admin/user_sessions/new.html.slim +28 -2
- data/lib/generators/trek/templates/views/admin/users/_passkeys.html.slim +67 -0
- data/lib/generators/trek/templates/views/admin/users/_properties.html.slim +21 -0
- data/lib/generators/trek/templates/views/admin/users/_sessions.html.slim +70 -0
- data/lib/generators/trek/templates/views/admin/users/index.html.slim +13 -0
- data/lib/generators/trek/templates/views/admin/users/show.html.slim +10 -21
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.en.html.slim +22 -0
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.en.text.erb +16 -0
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.fr.html.slim +22 -0
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.fr.text.erb +16 -0
- data/lib/generators/trek/templates/views/admin_mailer/sign_in_locked.en.html.slim +17 -0
- data/lib/generators/trek/templates/views/admin_mailer/sign_in_locked.en.text.erb +10 -0
- data/lib/generators/trek/templates/views/admin_mailer/sign_in_locked.fr.html.slim +18 -0
- data/lib/generators/trek/templates/views/admin_mailer/sign_in_locked.fr.text.erb +10 -0
- data/lib/trek/admin_ip_allowlist.rb +52 -0
- data/lib/trek/csp_reports.rb +99 -0
- data/lib/trek/data/common_passwords.txt +857 -0
- data/lib/trek/data/passkey_providers.json +404 -0
- data/lib/trek/disposable_email_domains.rb +53 -0
- data/lib/trek/engine.rb +12 -0
- data/lib/trek/passkey_providers.rb +19 -0
- data/lib/trek/reverse_dns.rb +27 -0
- data/lib/trek/version.rb +1 -1
- data/lib/trek.rb +13 -0
- data/package.json +3 -5
- data/renovate.json +11 -1
- data/trek.gemspec +3 -0
- data/yarn.lock +1757 -2019
- metadata +132 -4
- data/.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs +0 -541
- data/.yarn/releases/yarn-3.8.0.cjs +0 -875
- data/lib/generators/trek/templates/models/user_session.rb +0 -3
data/.yarnrc.yml
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
enableGlobalCache: false
|
|
2
2
|
|
|
3
|
-
|
|
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-
|
|
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.
|
|
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
|
|
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.
|
|
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;
|
|
@@ -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
|
+
}
|
data/app/components/trek/form/password_strength_component/password_strength_component.html.slim
ADDED
|
@@ -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 @@
|
|
|
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
|