trek 2.3.0 → 2.3.2

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +1 -1
  3. data/CHANGELOG.md +18 -0
  4. data/Gemfile.lock +4 -1
  5. data/app/components/trek/dropdown_component/dropdown_component.css +4 -1
  6. data/app/components/trek/dropdown_component/dropdown_component.html.slim +1 -1
  7. data/app/components/trek/dropdown_component.rb +5 -0
  8. data/app/components/trek/header_component/header_component.css +5 -6
  9. data/app/components/trek/header_component/header_component.html.slim +1 -1
  10. data/app/components/trek/header_component.rb +4 -0
  11. data/app/controllers/concerns/trek/filters.rb +18 -1
  12. data/app/controllers/concerns/trek/scopes.rb +9 -2
  13. data/app/models/concerns/trek/users/validations.rb +15 -6
  14. data/docs/.vitepress/config.mjs +20 -1
  15. data/docs/.vitepress/theme/style.css +36 -8
  16. data/docs/changelog.md +9 -0
  17. data/docs/index.md +7 -7
  18. data/docs/public/apple-touch-icon.png +0 -0
  19. data/docs/public/favicon.ico +0 -0
  20. data/docs/public/favicon.png +0 -0
  21. data/docs/public/trek-mark.webp +0 -0
  22. data/docs/reference/controllers.md +9 -4
  23. data/docs/reference/models/user.md +2 -2
  24. data/lib/generators/trek/templates/controllers/admin/user_password_resets_controller.rb +7 -0
  25. data/lib/generators/trek/templates/spec/models/user_spec.rb +33 -0
  26. data/lib/generators/trek/templates/spec/requests/admin/user_password_resets_spec.rb +19 -0
  27. data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +12 -0
  28. data/lib/generators/trek/templates/views/admin/user_password_resets/edit.html.slim +1 -1
  29. data/lib/generators/trek/templates/views/admin/user_password_resets/new.html.slim +1 -1
  30. data/lib/generators/trek/templates/views/admin/user_sessions/new.html.slim +1 -1
  31. data/lib/trek/version.rb +1 -1
  32. data/lib/trek.rb +1 -0
  33. data/package.json +1 -1
  34. data/trek.gemspec +1 -0
  35. metadata +20 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c42696637827a5c00e5b85274ade2eda636f54cd975af5f781c5f0eb6090e4e9
4
- data.tar.gz: 1860fefeba9c9a75a1c5caa7e1ec3b9a5907a914636f5c1e65be27ce9a5fe166
3
+ metadata.gz: 3749a9dc01b378419fbbaacdbf9e8251100d9bdd6281b308a9d44cf9dd18d182
4
+ data.tar.gz: d61b12a1f80494ebaf0c664bbfcfdf2d24b682632ef9c268117b4de23fb7e1b5
5
5
  SHA512:
6
- metadata.gz: c6da73ed50866c320bb0a16b91d6acb77d2c6eb43ec3d9bc5fdcbd2000560dcc7789929d6a01674cce34f1c53ba4db94da5454d8302d841987d20fa48594c0f4
7
- data.tar.gz: b5897444ef3f86a8f86f65b82180d20078e2220a5508cc8af5d87c36ff2c05cb43dc9f4b1ee30fea9afb43c3769754dfab4670127909a6a4788db213b276f9e8
6
+ metadata.gz: e9083e34869370a8b22b69a8c9aadb3f404e74f9d4b32097c48f51ad024d750aed71769a203112b78f19b74c7489ec46f512340bdd728dfebfef76f6bd37ec27
7
+ data.tar.gz: 9800e5d16a2639e2a51644ce67bb488bd1e4c34930867f08311b7df56992dd7dcf42b37a926d4097a5bd5f9a1a59bc476f5d2c70091b9eb2f54efc11ee14dcfc
data/AGENTS.md CHANGED
@@ -60,6 +60,6 @@ Security: `bundle exec brakeman` · `bundle exec bundle-audit check --update`
60
60
  - **Slim** linted via slim_lint; **JS/CSS** via ESLint + Stylelint + Prettier 2.8.8
61
61
  - **i18n**: keep English + French locales in sync (`config/locales/trek.*.yml`); DeepL auto-translation via `DEEPL_AUTH_KEY`
62
62
  - **Versioning**: version lives in both `lib/trek/version.rb` and `package.json` — keep in sync via `make release`
63
- - **CHANGELOG.md**: Keep-a-Changelog format. **Every MR must add an `[Unreleased]` entry.** 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.
63
+ - **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.
64
64
  - **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
65
  - **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,23 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.3.2] - 2026-10-02
4
+
5
+ ### Fixed
6
+
7
+ - **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.
8
+ - 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`.
9
+
10
+ ## [2.3.1] - 2026-10-02
11
+
12
+ ### Changed
13
+
14
+ - `Trek::Filters` only filters on the columns a controller lists in its new `available_filters` (empty by default), read through strong params (`filter_params`): other keys of `params[:filters]`, and values that aren't a single value, are ignored. Until now any key became a `where` condition, so a request could filter an index on columns it was never meant to expose. **Upgrade note:** in each controller including `Trek::Filters`, define `available_filters` returning the filtered columns (e.g. `def available_filters = %i[country_code]`), or its filters stop applying.
15
+ - `Trek::Users::Validations` stores emails stripped and in lower case (sign-in and password resets now ignore case), checks them with `email_validator`'s strict mode, and refuses a spaces-only password; the password reset form refuses an empty password instead of pretending to change it. **Upgrade note:** resolve users whose emails differ only by case (`User.group("lower(email)").having("count(*) > 1").count`), then downcase the rest (`User.find_each { |user| user.update_column(:email, user.email.strip.downcase) }`); in `Admin::UserPasswordResetsController#update`, redirect back with an error when `params[:password]` is blank (see Trek's template); re-run `rails g trek:install:model_specs` for the new specs.
16
+
17
+ ### Fixed
18
+
19
+ - The header's theme switcher now hovers like the links next to it, and the whole pill is clickable, not just the icon. `Trek::DropdownComponent`'s trigger inherits its context's color, and a new `trigger_classnames:` option styles the trigger button. **Upgrade note:** if you reuse `Trek::DropdownComponent`, move trigger styling (padding, hover) from `classnames:` to `trigger_classnames:`.
20
+
3
21
  ## [2.3.0] - 2026-10-01
4
22
 
5
23
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- trek (2.3.0)
4
+ trek (2.3.2)
5
5
  action_policy (~> 0.6)
6
6
  actioncable
7
7
  acts_as_list (~> 1.1)
@@ -10,6 +10,7 @@ PATH
10
10
  closure_tree (~> 9.0)
11
11
  deepl-rb (~> 3.0)
12
12
  dry-initializer (~> 3.1)
13
+ email_validator (~> 2.2)
13
14
  fastimage (~> 2.2)
14
15
  http_accept_language (~> 2.1)
15
16
  i18n-tasks (~> 1.0)
@@ -205,6 +206,8 @@ GEM
205
206
  dry-inflector (~> 1.0)
206
207
  dry-logic (~> 1.4)
207
208
  zeitwerk (~> 2.6)
209
+ email_validator (2.2.4)
210
+ activemodel
208
211
  erb (6.0.4)
209
212
  erubi (1.13.1)
210
213
  fastimage (2.4.0)
@@ -5,13 +5,16 @@
5
5
  position: relative;
6
6
  }
7
7
 
8
+ /* Inherits its context's color and font, so it reads as part of the surrounding text or link (and
9
+ follows its hover color). Style the trigger itself, not the root, with `trigger_classnames:`. */
8
10
  .trigger {
9
11
  align-items: center;
10
12
  background: none;
11
13
  border: none;
12
- color: var(--slate-12);
14
+ color: inherit;
13
15
  cursor: pointer;
14
16
  display: inline-flex;
17
+ font: inherit;
15
18
  gap: 0.4rem;
16
19
  }
17
20
 
@@ -1,7 +1,7 @@
1
1
  div(class=classes data-controller=identifier)
2
2
  button(
3
3
  type="button"
4
- class=class_for("trigger")
4
+ class=trigger_classes
5
5
  aria-haspopup="true"
6
6
  aria-expanded="false"
7
7
  aria-controls=menu_id
@@ -2,6 +2,7 @@ module Trek
2
2
  class DropdownComponent < Trek::Component
3
3
  option :label, optional: true # accessible name (aria-label on trigger + menu)
4
4
  option :classnames, optional: true # extra classes on root
5
+ option :trigger_classnames, optional: true # extra classes on the trigger button, the clickable element
5
6
  option :hide_chevron, default: proc { false }
6
7
 
7
8
  renders_one :trigger # inner content of the trigger button (e.g. an icon)
@@ -11,6 +12,10 @@ module Trek
11
12
  class_names_for(root_class, classnames)
12
13
  end
13
14
 
15
+ def trigger_classes
16
+ class_names_for(class_for("trigger"), trigger_classnames)
17
+ end
18
+
14
19
  def menu_id
15
20
  @menu_id ||= "#{identifier}-menu-#{SecureRandom.hex(4)}"
16
21
  end
@@ -66,13 +66,12 @@
66
66
  text-overflow: ellipsis;
67
67
  }
68
68
 
69
- /* Theme Dropdown */
70
- .nav-item-link:global(.c-trek--dropdown-root) {
71
- padding: 0.8rem 1.6rem;
72
- }
73
-
74
- .nav-item-link:global(.c-trek--dropdown-root .c-trek--icon-root) {
69
+ /* The theme dropdown's trigger: a nav item button (the button itself, so the whole pill is clickable),
70
+ with a larger icon and less vertical padding. */
71
+ .nav-theme-trigger {
75
72
  --icon-size: 2rem;
73
+
74
+ padding: 0.8rem 1.6rem;
76
75
  }
77
76
 
78
77
  /* Media queries */
@@ -35,7 +35,7 @@ div(class=classes data-controller=identifier \
35
35
  li(class=class_for("nav-item"))
36
36
  = render Trek::DropdownComponent.new( \
37
37
  label: User.human_attribute_name(:theme),
38
- classnames: class_for("nav-item-link"),
38
+ trigger_classnames: theme_trigger_classes,
39
39
  hide_chevron: true,
40
40
  ) do |dropdown|
41
41
  - dropdown.with_trigger do
@@ -29,6 +29,10 @@ module Trek
29
29
  ) && preview_path.present?
30
30
  end
31
31
 
32
+ def theme_trigger_classes
33
+ class_names_for("nav-item-button", "nav-theme-trigger", map_args: true)
34
+ end
35
+
32
36
  def theme_item_data(theme)
33
37
  { action: stimulus_action("click", "setTheme"), theme: }
34
38
  end
@@ -25,8 +25,13 @@ module Trek
25
25
  def apply_filters(scope, filters)
26
26
  return scope if filters.blank?
27
27
 
28
+ # Only the controller's own filters: the keys come from the request and become column names
29
+ allowed = available_filters.map(&:to_s)
30
+
28
31
  filters.each do |filter, value|
29
32
  next if value.blank?
33
+ next unless allowed.include?(filter.to_s)
34
+
30
35
  value = nil if value == "nil"
31
36
 
32
37
  scope = scope.where(filter => value)
@@ -37,7 +42,19 @@ module Trek
37
42
 
38
43
  def apply_filters!
39
44
  @filters_applied = true
40
- @objects = apply_filters(@objects, params[:filters])
45
+ @objects = apply_filters(@objects, filter_params)
46
+ end
47
+
48
+ # The requested filters, permitted to the available ones with scalar values only; nil when
49
+ # params[:filters] isn't a hash
50
+ def filter_params
51
+ filters = params[:filters]
52
+ filters.permit(*available_filters) if filters.respond_to?(:permit)
53
+ end
54
+
55
+ # The columns a request may filter on; override in the controller.
56
+ def available_filters
57
+ []
41
58
  end
42
59
 
43
60
  private
@@ -19,8 +19,13 @@ module Trek
19
19
  def apply_scopes(scope, filters)
20
20
  return scope if filters.blank?
21
21
 
22
+ # Only the controller's own scopes: the names come from the request, and a relation also
23
+ # answers to destroy_all, delete_all…
24
+ allowed = available_scopes.map(&:to_s)
25
+
22
26
  filters.each do |filter, value|
23
27
  next if value.blank?
28
+ next unless allowed.include?(filter.to_s)
24
29
 
25
30
  scope = scope.public_send(filter) if scope.respond_to?(filter)
26
31
  end
@@ -31,7 +36,7 @@ module Trek
31
36
  def apply_scopes!
32
37
  @scopes_applied = true
33
38
  @unscoped_objects = @objects
34
- @objects = apply_scopes(@objects, params[:scopes])
39
+ @objects = apply_scopes(@objects, scope_params)
35
40
  scope_counts
36
41
  end
37
42
 
@@ -65,8 +70,10 @@ module Trek
65
70
  []
66
71
  end
67
72
 
73
+ # The requested scopes, permitted to the available ones; nil when params[:scopes] isn't a hash
68
74
  def scope_params
69
- params[:scopes]&.permit(*available_scopes)
75
+ scopes = params[:scopes]
76
+ scopes.permit(*available_scopes) if scopes.respond_to?(:permit)
70
77
  end
71
78
 
72
79
  private def ensure_scopes_applied
@@ -4,14 +4,23 @@ module Trek
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
7
- validates :password,
8
- length: { minimum: 8 },
9
- if: -> { new_record? || password_digest_changed? },
10
- allow_blank: true
7
+ # Also applies to lookups (`find_by(email:)`, `authenticate_by`), so signing in and asking
8
+ # for a password reset work whatever the case typed.
9
+ normalizes :email, with: ->(email) { email.strip.downcase }
10
+
11
11
  validates :email,
12
12
  presence: true,
13
- uniqueness: true,
14
- format: { with: URI::MailTo::EMAIL_REGEXP }
13
+ uniqueness: { case_sensitive: false },
14
+ email: { mode: :strict }
15
+ # Only when a password was assigned: `password` is nil otherwise.
16
+ validates :password, length: { minimum: 8 }, allow_nil: true
17
+ validate :password_not_only_spaces
18
+ end
19
+
20
+ private
21
+
22
+ def password_not_only_spaces
23
+ errors.add(:password, :blank) if password&.strip&.empty?
15
24
  end
16
25
  end
17
26
  end
@@ -1,5 +1,9 @@
1
1
  import { defineConfig } from 'vitepress'
2
2
  import llmstxt from 'vitepress-plugin-llms'
3
+ import { readFileSync } from 'node:fs'
4
+
5
+ // The released version: `make release` keeps package.json in sync with lib/trek/version.rb.
6
+ const { version } = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'))
3
7
 
4
8
 
5
9
  // https://vitepress.dev/reference/site-config
@@ -11,6 +15,13 @@ export default defineConfig({
11
15
  description: "A modern content management & back-office system for Ruby on Rails",
12
16
  lang: 'en-US',
13
17
  lastUpdated: true,
18
+ // The header logo doubles as the favicon (PNG versions for older browsers and iOS home screens,
19
+ // favicon.ico for clients that only request /favicon.ico).
20
+ head: [
21
+ ['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }],
22
+ ['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon.png' }],
23
+ ['link', { rel: 'apple-touch-icon', href: '/apple-touch-icon.png' }],
24
+ ],
14
25
  themeConfig: {
15
26
  logo: { src: '/logo.svg', width: 24, height: 24 },
16
27
 
@@ -18,7 +29,15 @@ export default defineConfig({
18
29
  nav: [
19
30
  { text: 'Home', link: '/' },
20
31
  { text: 'Guide', link: '/what-is-trek' },
21
- { text: 'Reference', link: '/reference/' }
32
+ { text: 'Reference', link: '/reference/' },
33
+ {
34
+ text: `v${version}`,
35
+ items: [
36
+ { text: 'Changelog', link: '/changelog' },
37
+ { text: 'RubyGems', link: `https://rubygems.org/gems/trek/versions/${version}` },
38
+ { text: 'npm', link: `https://www.npmjs.com/package/trek/v/${version}` }
39
+ ]
40
+ }
22
41
  ],
23
42
 
24
43
  sidebar: {
@@ -75,15 +75,18 @@
75
75
  * -------------------------------------------------------------------------- */
76
76
 
77
77
  :root {
78
+ /* The violet end of the hero name gradient, a step darker on hover and when pressed */
79
+ --trek-button-brand: #7a4df0;
80
+
78
81
  --vp-button-brand-border: transparent;
79
82
  --vp-button-brand-text: var(--vp-c-white);
80
- --vp-button-brand-bg: var(--vp-c-brand-3);
83
+ --vp-button-brand-bg: var(--trek-button-brand);
81
84
  --vp-button-brand-hover-border: transparent;
82
85
  --vp-button-brand-hover-text: var(--vp-c-white);
83
- --vp-button-brand-hover-bg: var(--vp-c-brand-2);
86
+ --vp-button-brand-hover-bg: color-mix(in srgb, var(--trek-button-brand) 88%, black);
84
87
  --vp-button-brand-active-border: transparent;
85
88
  --vp-button-brand-active-text: var(--vp-c-white);
86
- --vp-button-brand-active-bg: var(--vp-c-brand-1);
89
+ --vp-button-brand-active-bg: color-mix(in srgb, var(--trek-button-brand) 76%, black);
87
90
  }
88
91
 
89
92
  /**
@@ -91,19 +94,44 @@
91
94
  * -------------------------------------------------------------------------- */
92
95
 
93
96
  :root {
97
+ --trek-green: #63e0a6;
98
+ --trek-purple: #9b7bff;
99
+
94
100
  --vp-home-hero-name-color: transparent;
95
- --vp-home-hero-name-background: -webkit-linear-gradient(
101
+ /* Darker stops on the light background, the brand colors themselves on the dark one */
102
+ --vp-home-hero-name-background: linear-gradient(
96
103
  120deg,
97
- #bd34fe 30%,
98
- #41d1ff
104
+ #1fa971 15%,
105
+ #7a4df0 100%
99
106
  );
100
107
 
101
108
  --vp-home-hero-image-background-image: linear-gradient(
102
109
  -45deg,
103
- #bd34fe 50%,
104
- #47caff 50%
110
+ var(--trek-purple) 50%,
111
+ var(--trek-green) 50%
105
112
  );
106
113
  --vp-home-hero-image-filter: blur(44px);
114
+
115
+ /* Feature icons (docs/index.md): the ink follows the text color, light or dark */
116
+ --trek-icon-ink: currentColor;
117
+ --trek-icon-accent: var(--trek-green);
118
+ }
119
+
120
+ /* Text selection in the brand violet rather than the browser's default blue */
121
+ ::selection {
122
+ background-color: color-mix(in srgb, var(--trek-purple) 30%, transparent);
123
+ }
124
+
125
+ .dark ::selection {
126
+ background-color: color-mix(in srgb, var(--trek-purple) 45%, transparent);
127
+ }
128
+
129
+ .dark {
130
+ --vp-home-hero-name-background: linear-gradient(
131
+ 120deg,
132
+ var(--trek-green) 15%,
133
+ var(--trek-purple) 100%
134
+ );
107
135
  }
108
136
 
109
137
  @media (min-width: 640px) {
data/docs/changelog.md ADDED
@@ -0,0 +1,9 @@
1
+ ---
2
+ outline: 2
3
+ ---
4
+
5
+ # Changelog
6
+
7
+ What changed in each release of Trek, newest first, with upgrade notes when an app needs to act. **Unreleased** lists what's merged but not yet published.
8
+
9
+ <!--@include: ../CHANGELOG.md-->
data/docs/index.md CHANGED
@@ -7,7 +7,7 @@ hero:
7
7
  text: "Take the easy path"
8
8
  tagline: "A modern content management & back-office system for Ruby on Rails"
9
9
  image:
10
- src: /logo.svg
10
+ src: /trek-mark.webp
11
11
  alt: Trek
12
12
  actions:
13
13
  - theme: brand
@@ -18,22 +18,22 @@ hero:
18
18
  link: /getting-started
19
19
 
20
20
  features:
21
- - icon: 🚀
21
+ - icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M16 14A8 8 0 0 1 32 14V38H16ZM21 18A3 3 0 1 0 27 18A3 3 0 1 0 21 18ZM16 26V38H4A12 12 0 0 1 16 26ZM32 26A12 12 0 0 1 44 38H32Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M18 40H30A6 6 0 0 1 18 40Z"/></svg>'
22
22
  title: Ready out of the box
23
23
  details: One installer sets up authentication, authorization, file uploads, emails, i18n and a polished admin UI — with sensible conventions you can override later.
24
- - icon: 📝
24
+ - icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M6 6H30V30A12 12 0 0 1 18 42H6ZM12 14H24V18H12ZM12 22H20V26H12Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M34 6H42V32A4 4 0 0 1 34 32Z"/></svg>'
25
25
  title: Modern content editing
26
26
  details: A Tiptap-based rich text editor, hierarchical pages with versioning and SEO-friendly paths, plus reusable content fragments.
27
- - icon: 🛠
27
+ - icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M6 18H30V30H42V42H6Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M6 18V6A12 12 0 0 1 18 18Z"/></svg>'
28
28
  title: Scaffold-driven development
29
29
  details: One command generates the model, admin panel, views, policy, routes and locales for any resource — just like Rails scaffolding, but for your back-office.
30
- - icon: 🌍
30
+ - icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M22 22H5A17 17 0 0 1 22 5ZM22 26V43A17 17 0 0 1 5 26ZM26 26H43A17 17 0 0 1 26 43Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M26 22V5A17 17 0 0 1 43 22Z"/></svg>'
31
31
  title: Multilingual by default
32
32
  details: Content and admin UI translations powered by Mobility and i18n-tasks, with optional automatic translation via DeepL.
33
- - icon: 🔐
33
+ - icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M14 22V16A10 10 0 0 1 34 16V22H28V16A4 4 0 0 0 20 16V22ZM8 22H24V42H16A8 8 0 0 1 8 34Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M24 22H40V34A8 8 0 0 1 32 42H24Z"/></svg>'
34
34
  title: Roles & policies built in
35
35
  details: Authentication on Rails' own has_secure_password and ActionPolicy authorization with admin, editor, user, reader and guest roles, enforced across every panel.
36
- - icon: 🧩
36
+ - icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M6 6H22V22H6ZM26 22V6A16 16 0 0 1 42 22ZM6 26H22V42A16 16 0 0 1 6 26Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M26 26H42V42H26Z"/></svg>'
37
37
  title: Hotwire-native UI
38
38
  details: ViewComponents, Stimulus and Turbo under the hood, with light & dark themes, Radix colors and a customizable design system.
39
39
  ---
Binary file
Binary file
Binary file
Binary file
@@ -26,9 +26,10 @@ end
26
26
 
27
27
  ### Filters
28
28
 
29
- `Trek::Filters` lets users narrow an index by attribute values. Call `apply_filters!` in your `index` action — it
30
- rewrites `@objects` from `params[:filters]`, applying a `where(column => value)` per non-blank filter (a value of
31
- `"nil"` matches `NULL`):
29
+ `Trek::Filters` lets users narrow an index by attribute values. Declare the columns users may filter on via
30
+ `available_filters`, then call `apply_filters!` in your `index` action — it rewrites `@objects` from
31
+ `params[:filters]`, applying a `where(column => value)` per non-blank filter (a value of `"nil"` matches `NULL`). The filters are read
32
+ through strong params (`filter_params`): any other key, and any value that isn't a single value, is ignored:
32
33
 
33
34
  ```ruby
34
35
  module Admin
@@ -44,6 +45,10 @@ module Admin
44
45
 
45
46
  private
46
47
 
48
+ def available_filters
49
+ %i[country_code]
50
+ end
51
+
47
52
  def countries
48
53
  City.distinct.pluck(:country_code).index_with { |code| I18n.t("countries.#{code}") }
49
54
  end
@@ -70,7 +75,7 @@ with the chosen filter:
70
75
  ### Scopes
71
76
 
72
77
  `Trek::Scopes` filters by **named model scopes** rather than raw attributes. Declare the allowed scopes via
73
- `available_scopes`, then call `apply_scopes!` in your `index`. It also exposes `scope_counts` and
78
+ `available_scopes`, then call `apply_scopes!` in your `index`; any other name in `params[:scopes]` is ignored. It also exposes `scope_counts` and
74
79
  `unscoped_objects` helpers (the per-scope match counts and the pre-scope collection) for building scope tabs:
75
80
 
76
81
  ```ruby
@@ -52,8 +52,8 @@ User.create(email: "new@example.com", role: :editor, send_invite: true)
52
52
 
53
53
  `Trek::Users::Validations` enforces:
54
54
 
55
- - `email` — presence, uniqueness, valid format
56
- - `password` — minimum 8 characters (on creation or password change)
55
+ - `email` — stored stripped and in lower case (lookups too, so sign-in and password resets ignore case), present, unique whatever its case, and well-formed by [`email_validator`](https://github.com/K-and-R/email_validator)'s strict mode (no consecutive or leading/trailing dots, a domain with a TLD)
56
+ - `password` — when one is assigned: at least 8 characters, and not spaces only. A blank password is ignored by `has_secure_password` (the current one is kept); the password reset form refuses it
57
57
 
58
58
  ## Scopes
59
59
 
@@ -29,6 +29,13 @@ module Admin
29
29
  end
30
30
 
31
31
  def update
32
+ # has_secure_password ignores a blank password: refuse it, rather than report a change that
33
+ # didn't happen.
34
+ if params[:password].blank?
35
+ @user.errors.add(:password, :blank)
36
+ return redirect_to edit_admin_user_password_reset_path(@token), alert: @user.errors.full_messages.join(", ")
37
+ end
38
+
32
39
  @user.update!(params.permit(:password))
33
40
  @user.user_sessions.destroy_all
34
41
  redirect_to new_admin_user_session_path, notice: t(".success")
@@ -26,6 +26,27 @@ RSpec.describe User do
26
26
  expect(user).to be_invalid
27
27
  end
28
28
 
29
+ it "requires a unique email whatever its case" do
30
+ users.create(email: "ada@example.com")
31
+ user.email = "Ada@Example.com"
32
+
33
+ expect(user).to be_invalid
34
+ end
35
+
36
+ %w[ada..lovelace@example.com .ada@example.com ada.@example.com ada@example..com ada@example].each do |email|
37
+ it "refuses an email mail providers would reject: #{email}" do
38
+ user.email = email
39
+
40
+ expect(user).to be_invalid
41
+ end
42
+ end
43
+
44
+ it "refuses a password made of spaces only" do
45
+ user.password = " " * 8
46
+
47
+ expect(user).to be_invalid
48
+ end
49
+
29
50
  it "requires a password of at least 8 characters on creation" do
30
51
  user.password = "short"
31
52
 
@@ -47,6 +68,18 @@ RSpec.describe User do
47
68
  end
48
69
  end
49
70
 
71
+ describe "email" do
72
+ it "is stored in lower case, without surrounding spaces" do
73
+ expect(users.create(email: " Ada@Example.com ").email).to eq("ada@example.com")
74
+ end
75
+
76
+ it "finds the user whatever its case" do
77
+ ada = users.create(email: "ada@example.com")
78
+
79
+ expect(described_class.find_by(email: "ADA@example.com")).to eq(ada)
80
+ end
81
+ end
82
+
50
83
  describe "roles" do
51
84
  it "defaults to guest" do
52
85
  expect(user.role).to eq("guest")
@@ -9,6 +9,12 @@ RSpec.describe "Admin password resets", type: :request do
9
9
  expect(response).to have_http_status(:ok)
10
10
  end
11
11
 
12
+ it "keeps the reset request actions in the card, out of the sticky action bar" do
13
+ get new_admin_user_password_reset_path
14
+
15
+ expect(response.body).to include("c-trek--form--actions-aspect-seamless")
16
+ end
17
+
12
18
  it "emails a reset link to a privileged user" do
13
19
  expect { post admin_user_password_resets_path, params: { email: editor.email } }
14
20
  .to have_enqueued_mail(Admin::PasswordsMailer, :reset)
@@ -31,6 +37,12 @@ RSpec.describe "Admin password resets", type: :request do
31
37
  expect(response).to have_http_status(:ok)
32
38
  end
33
39
 
40
+ it "keeps the new password actions in the card, out of the sticky action bar" do
41
+ get edit_admin_user_password_reset_path(editor.password_reset_token)
42
+
43
+ expect(response.body).to include("c-trek--form--actions-aspect-seamless")
44
+ end
45
+
34
46
  it "renders the new password form from an invitation link" do
35
47
  get edit_admin_user_password_reset_path(editor.generate_token_for(:invite))
36
48
 
@@ -56,6 +68,13 @@ RSpec.describe "Admin password resets", type: :request do
56
68
  expect(editor.user_sessions).to be_empty
57
69
  end
58
70
 
71
+ it "sends an empty password back to the form rather than pretending to change it" do
72
+ token = editor.password_reset_token
73
+ patch admin_user_password_reset_path(token), params: { password: "" }
74
+
75
+ expect(response).to redirect_to(edit_admin_user_password_reset_path(token))
76
+ end
77
+
59
78
  it "sends a password shorter than 8 characters back to the form" do
60
79
  token = editor.password_reset_token
61
80
  patch admin_user_password_reset_path(token), params: { password: "short" }
@@ -9,6 +9,12 @@ RSpec.describe "Admin sign-in", type: :request do
9
9
  expect(response).to have_http_status(:ok)
10
10
  end
11
11
 
12
+ it "keeps the sign-in actions in the card, out of the sticky action bar" do
13
+ get new_admin_user_session_path
14
+
15
+ expect(response.body).to include("c-trek--form--actions-aspect-seamless")
16
+ end
17
+
12
18
  it "signs a privileged user in and opens the dashboard" do
13
19
  post admin_user_session_path, params: { email: editor.email, password: users.password }
14
20
  follow_redirect!
@@ -16,6 +22,12 @@ RSpec.describe "Admin sign-in", type: :request do
16
22
  expect(response).to have_http_status(:ok)
17
23
  end
18
24
 
25
+ it "signs in whatever the case of the email" do
26
+ post admin_user_session_path, params: { email: editor.email.upcase, password: users.password }
27
+
28
+ expect(response).to redirect_to(admin_root_url)
29
+ end
30
+
19
31
  it "refuses a wrong password" do
20
32
  post admin_user_session_path, params: { email: editor.email, password: "not the password" }
21
33
 
@@ -7,7 +7,7 @@
7
7
  maxlength: 72,
8
8
  minlength: 8
9
9
 
10
- = render Trek::Form::ActionsComponent.new do |c|
10
+ = render Trek::Form::ActionsComponent.new(aspect: :seamless) do |c|
11
11
  ruby:
12
12
  c.with_button(
13
13
  type: "submit",
@@ -6,7 +6,7 @@
6
6
  autofocus: true,
7
7
  autocomplete: "username"
8
8
 
9
- = render Trek::Form::ActionsComponent.new do |c|
9
+ = render Trek::Form::ActionsComponent.new(aspect: :seamless) do |c|
10
10
  ruby:
11
11
  c.with_button(
12
12
  type: "submit",
@@ -15,7 +15,7 @@
15
15
  maxlength: 72,
16
16
  minlength: 8
17
17
 
18
- = render Trek::Form::ActionsComponent.new do |c|
18
+ = render Trek::Form::ActionsComponent.new(aspect: :seamless) do |c|
19
19
  ruby:
20
20
  c.with_button(
21
21
  type: "submit",
data/lib/trek/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Trek
4
- VERSION = "2.3.0"
4
+ VERSION = "2.3.2"
5
5
  end
data/lib/trek.rb CHANGED
@@ -6,6 +6,7 @@ require "blurhash"
6
6
  require "closure_tree"
7
7
  require "deepl"
8
8
  require "dry-initializer"
9
+ require "email_validator"
9
10
  require "fastimage"
10
11
  require "http_accept_language"
11
12
  require "image_processing"
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trek",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "description": "A modern content management & back-office system for Ruby on Rails",
5
5
  "homepage": "https://trek.etamin.studio",
6
6
  "license": "MIT",
data/trek.gemspec CHANGED
@@ -35,6 +35,7 @@ Gem::Specification.new do |spec|
35
35
  spec.add_dependency "closure_tree", "~> 9.0"
36
36
  spec.add_dependency "deepl-rb", "~> 3.0"
37
37
  spec.add_dependency "dry-initializer", "~> 3.1"
38
+ spec.add_dependency "email_validator", "~> 2.2"
38
39
  spec.add_dependency "fastimage", "~> 2.2"
39
40
  spec.add_dependency "http_accept_language", "~> 2.1"
40
41
  spec.add_dependency "i18n-tasks", "~> 1.0"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trek
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 2.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mohamed Bengrich
@@ -124,6 +124,20 @@ dependencies:
124
124
  - - "~>"
125
125
  - !ruby/object:Gem::Version
126
126
  version: '3.1'
127
+ - !ruby/object:Gem::Dependency
128
+ name: email_validator
129
+ requirement: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - "~>"
132
+ - !ruby/object:Gem::Version
133
+ version: '2.2'
134
+ type: :runtime
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - "~>"
139
+ - !ruby/object:Gem::Version
140
+ version: '2.2'
127
141
  - !ruby/object:Gem::Dependency
128
142
  name: fastimage
129
143
  requirement: !ruby/object:Gem::Requirement
@@ -781,11 +795,16 @@ files:
781
795
  - docs/.vitepress/config.mjs
782
796
  - docs/.vitepress/theme/index.js
783
797
  - docs/.vitepress/theme/style.css
798
+ - docs/changelog.md
784
799
  - docs/customize.md
785
800
  - docs/deploy.md
786
801
  - docs/getting-started.md
787
802
  - docs/index.md
803
+ - docs/public/apple-touch-icon.png
804
+ - docs/public/favicon.ico
805
+ - docs/public/favicon.png
788
806
  - docs/public/logo.svg
807
+ - docs/public/trek-mark.webp
789
808
  - docs/reference/components.md
790
809
  - docs/reference/concerns/contentable.md
791
810
  - docs/reference/concerns/formattable.md