lato 3.24.0 → 3.26.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +49 -5
  3. data/app/assets/javascripts/lato/controllers/lato_account_security_controller.js +51 -0
  4. data/app/assets/javascripts/lato/controllers/lato_account_webauthn_controller.js +8 -5
  5. data/app/assets/javascripts/lato/controllers/lato_action_controller.js +26 -15
  6. data/app/assets/javascripts/lato/controllers/lato_confirm_controller.js +25 -15
  7. data/app/assets/javascripts/lato/controllers/lato_copyclip_controller.js +28 -10
  8. data/app/assets/javascripts/lato/controllers/lato_form_controller.js +1 -0
  9. data/app/assets/javascripts/lato/controllers/lato_index_controller.js +4 -1
  10. data/app/controllers/concerns/lato/componentable.rb +20 -8
  11. data/app/controllers/concerns/lato/sessionable.rb +58 -16
  12. data/app/controllers/lato/account_controller.rb +98 -24
  13. data/app/controllers/lato/application_controller.rb +25 -1
  14. data/app/controllers/lato/authentication_controller.rb +74 -39
  15. data/app/helpers/lato/components_helper.rb +42 -16
  16. data/app/jobs/lato/active_storage_cleaner_job.rb +8 -7
  17. data/app/models/lato/invitation.rb +1 -1
  18. data/app/models/lato/operation.rb +6 -2
  19. data/app/models/lato/session.rb +73 -19
  20. data/app/models/lato/session_record.rb +5 -0
  21. data/app/models/lato/user.rb +66 -26
  22. data/app/views/lato/account/_alert-accepted-privacy-policy-version.html.erb +1 -1
  23. data/app/views/lato/account/_alert-accepted-terms-and-conditions-version.html.erb +1 -1
  24. data/app/views/lato/account/_form-authenticator.html.erb +19 -33
  25. data/app/views/lato/account/_form-destroy.html.erb +1 -0
  26. data/app/views/lato/account/_form-password.html.erb +4 -2
  27. data/app/views/lato/account/_form-user.html.erb +1 -0
  28. data/app/views/lato/account/_form-web3.html.erb +5 -3
  29. data/app/views/lato/account/_form-webauthn.html.erb +1 -0
  30. data/app/views/lato/account/_security-confirmation.html.erb +12 -0
  31. data/app/views/lato/authentication/_fields-registration.html.erb +8 -7
  32. data/app/views/lato/authentication/_form-authenticator.html.erb +2 -2
  33. data/app/views/lato/authentication/_form-signin.html.erb +2 -2
  34. data/app/views/lato/authentication/_form-verify-email.html.erb +3 -2
  35. data/app/views/lato/authentication/_form-web3-signin.html.erb +4 -3
  36. data/app/views/lato/components/_index.html.erb +25 -20
  37. data/app/views/lato/components/_navbar_nav_item.html.erb +1 -1
  38. data/app/views/lato/components/_operation.html.erb +4 -1
  39. data/app/views/lato/components/_page_head.html.erb +1 -1
  40. data/app/views/lato/components/_sidebar_nav_item.html.erb +2 -1
  41. data/app/views/lato/mailer/invitation/invite_mail.html.erb +1 -1
  42. data/app/views/lato/mailer/user/email_verification_mail.html.erb +1 -1
  43. data/app/views/lato/mailer/user/password_update_mail.html.erb +1 -1
  44. data/app/views/lato/mailer/user/signin_success_mail.html.erb +1 -1
  45. data/app/views/layouts/lato/_action.html.erb +2 -2
  46. data/app/views/layouts/lato/_aside-opener.html.erb +2 -2
  47. data/app/views/layouts/lato/_confirm.html.erb +6 -8
  48. data/app/views/layouts/lato/_feedbacks.html.erb +9 -8
  49. data/app/views/layouts/lato/_mailer-head_content.html.erb +1 -1
  50. data/app/views/layouts/lato/_network.html.erb +3 -3
  51. data/app/views/layouts/lato/application.html.erb +3 -2
  52. data/config/locales/en.yml +32 -0
  53. data/config/locales/fr.yml +29 -0
  54. data/config/locales/it.yml +29 -0
  55. data/config/locales/ro.yml +29 -0
  56. data/config/routes.rb +1 -0
  57. data/db/migrate/20261024000000_create_lato_session_records.rb +16 -0
  58. data/db/migrate/20261110000000_add_second_factor_counters_to_lato_users.rb +6 -0
  59. data/lib/lato/config.rb +4 -0
  60. data/lib/lato/engine.rb +9 -0
  61. data/lib/lato/version.rb +1 -1
  62. metadata +34 -17
@@ -12,7 +12,7 @@ user ||= Lato::User.new
12
12
  <% if session[:web3_nonce] %>
13
13
 
14
14
  <div class="alert alert-light mb-0 text-center">
15
- <h4 class="alert-heading">Connecting..</h4>
15
+ <h4 class="alert-heading" role="status"><%= I18n.t('lato.connecting') %></h4>
16
16
  <div class="progress" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100">
17
17
  <div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 100%"></div>
18
18
  </div>
@@ -21,13 +21,14 @@ user ||= Lato::User.new
21
21
  <span id="account_form-web3__nonce" style="display: none;"><%= session[:web3_nonce] %></span>
22
22
  <%= form.hidden_field :web3_address, id: 'account_form-web3__input-web3_address' %>
23
23
  <%= form.hidden_field :web3_signed_nonce, id: 'account_form-web3__input-web3_signed_nonce' %>
24
- <%= lato_form_submit form, 'Confirm', class: %w[btn-primary d-none], id: 'account_form-web3__submit' %>
24
+ <%= lato_form_submit form, I18n.t('lato.confirm'), class: %w[btn-primary d-none], id: 'account_form-web3__submit' %>
25
25
 
26
26
  <script>
27
27
  (async () => {
28
+ // Pinned exact version: dynamic import() can not carry an SRI hash (see README security checklist).
28
29
  const ethers = await import('https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.esm.min.js')
29
30
 
30
- const nonce = document.getElementById('account_form-web3__nonce').innerText
31
+ const nonce = document.getElementById('account_form-web3__nonce').textContent
31
32
  const inputAddress = document.getElementById('account_form-web3__input-web3_address')
32
33
  const inputSignedNonce = document.getElementById('account_form-web3__input-web3_signed_nonce')
33
34
  const submitButton = document.getElementById('account_form-web3__submit')
@@ -9,9 +9,10 @@ total_pages = collection.empty? ? params["#{key}_page"]&.to_i : 999_999_999 if s
9
9
 
10
10
  <%= turbo_frame_tag "lato_index_#{key}_#{model_name_underscore}", class: "lato-index #{browser.device.mobile? ? 'lato-index-mob' : 'lato-index-desk'}" do %>
11
11
 
12
- <%= form_tag request.path, method: :get, data: { controller: 'lato-form lato-index', turbo_frame: '_self' } do %>
12
+ <%= form_tag request.path, method: :get, data: { controller: 'lato-form lato-index', lato_index_key_value: key, turbo_frame: '_self' } do %>
13
13
  <%= hidden_field_tag :key, key %>
14
- <% params.each do |k, v| %>
14
+ <% params.except(:controller, :action, :key).each do |k, v| %>
15
+ <% next unless v.is_a?(String) %>
15
16
  <%= hidden_field_tag k, v %>
16
17
  <% end %>
17
18
 
@@ -33,7 +34,7 @@ total_pages = collection.empty? ? params["#{key}_page"]&.to_i : 999_999_999 if s
33
34
  <% if custom_actions.any? %>
34
35
  <div class="btn-group ms-2">
35
36
  <% custom_actions.each do |action_key, action_params| %>
36
- <%= link_to action_params[:path], { class: 'btn btn-primary' }.merge(action_params) do %>
37
+ <%= link_to action_params[:path], { class: 'btn btn-primary', 'aria-label': action_params[:title] }.merge(action_params) do %>
37
38
  <i class="<%= action_params[:icon] %>"></i>
38
39
  <% end %>
39
40
  <% end %>
@@ -42,7 +43,7 @@ total_pages = collection.empty? ? params["#{key}_page"]&.to_i : 999_999_999 if s
42
43
 
43
44
  <% if dropdown_actions %>
44
45
  <div class="dropdown ms-2">
45
- <%= content_tag :button, { class: 'btn btn-primary dropdown-toggle', 'data-bs-toggle': 'dropdown' }.merge(dropdown_actions) do %>
46
+ <%= content_tag :button, { class: 'btn btn-primary dropdown-toggle', 'data-bs-toggle': 'dropdown', 'aria-label': dropdown_actions[:title] }.merge(dropdown_actions) do %>
46
47
  <i class="<%= dropdown_actions[:icon] %>"></i>
47
48
  <% end %>
48
49
  <ul class="dropdown-menu">
@@ -109,28 +110,32 @@ total_pages = collection.empty? ? params["#{key}_page"]&.to_i : 999_999_999 if s
109
110
  <thead>
110
111
  <tr class="align-middle">
111
112
  <% columns.each do |column| %>
112
- <th scope="col" class="lato-index-desk-col-label lato-index-desk-col-label-<%= column %>">
113
+ <% column_label = collection_test_istance.respond_to?(column) ? collection.model.human_attribute_name(column) : lato_index_dynamic_label({
114
+ key: key,
115
+ model_name: model_name,
116
+ column: column
117
+ }) %>
118
+ <% sort_direction = sortable_columns.include?(column) ? { "#{column}|ASC" => 'ascending', "#{column}|DESC" => 'descending' }[params["#{key}_sort_by"]] : nil %>
119
+ <%= content_tag :th, scope: 'col', class: "lato-index-desk-col-label lato-index-desk-col-label-#{column}", 'aria-sort': sort_direction do %>
113
120
  <div class="d-flex align-items-center justify-content-between">
114
- <span><%= collection_test_istance.respond_to?(column) ? collection.model.human_attribute_name(column) : lato_index_dynamic_label({
115
- key: key,
116
- model_name: model_name,
117
- column: column
118
- }) %></span>
121
+ <span><%= column_label %></span>
119
122
  <% if sortable_columns.include?(column) %>
120
123
  <div class="btn-group ms-3 lato-index-col-sort">
121
- <div class="position-relative btn btn-sm <%= params["#{key}_sort_by"] == "#{column}|ASC" ? 'btn-primary' : 'btn-outline-primary' %>">
122
- <input type="submit" name="<%= "#{key}_sort_by" %>" value="<%= column %>|ASC" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0">
123
- <i class="bi bi-sort-up"></i>
124
- </div>
125
-
126
- <div class="position-relative btn btn-sm <%= params["#{key}_sort_by"] == "#{column}|DESC" ? 'btn-primary' : 'btn-outline-primary' %>">
127
- <input type="submit" name="<%= "#{key}_sort_by" %>" value="<%= column %>|DESC" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0">
128
- <i class="bi bi-sort-down"></i>
129
- </div>
124
+ <% [['ASC', 'ascending', 'bi-sort-up'], ['DESC', 'descending', 'bi-sort-down']].each do |direction, direction_name, icon| %>
125
+ <% active = params["#{key}_sort_by"] == "#{column}|#{direction}" %>
126
+ <button
127
+ type="submit"
128
+ name="<%= "#{key}_sort_by" %>"
129
+ value="<%= column %>|<%= direction %>"
130
+ class="btn btn-sm <%= active ? 'btn-primary' : 'btn-outline-primary' %>"
131
+ aria-label="<%= I18n.t("lato.sort_#{direction_name}", column: strip_tags(column_label.to_s)) %>"
132
+ aria-pressed="<%= active %>"
133
+ ><i class="bi <%= icon %>" aria-hidden="true"></i></button>
134
+ <% end %>
130
135
  </div>
131
136
  <% end %>
132
137
  </div>
133
- </th>
138
+ <% end %>
134
139
  <% end %>
135
140
  </tr>
136
141
  </thead>
@@ -1,5 +1,5 @@
1
1
  <li class="nav-item">
2
- <%= link_to path, class: ['nav-link', ('active' if active)] do %>
2
+ <%= link_to path, class: ['nav-link', ('active' if active)], aria: { current: ('page' if active) } do %>
3
3
  <%= yield %>
4
4
  <% end %>
5
5
  </li>
@@ -47,7 +47,10 @@
47
47
 
48
48
  <% if operation.output_action? %>
49
49
  <div class="mt-3 d-flex justify-content-center">
50
- <%= link_to operation.output_action['label'], operation.output_action['href'], { class: 'btn btn-primary' }.merge(operation.output_action['attributes']) %>
50
+ <%# Only http(s) and relative links: a javascript: href built from job input would run on click. %>
51
+ <% output_href = operation.output_action['href'].to_s %>
52
+ <% output_href = '#' unless output_href.start_with?('/', 'http://', 'https://') && !output_href.start_with?('//') %>
53
+ <%= link_to operation.output_action['label'], output_href, { class: 'btn btn-primary' }.merge(operation.output_action['attributes'] || {}) %>
51
54
  </div>
52
55
  <% end %>
53
56
  </div>
@@ -9,7 +9,7 @@
9
9
  </ol>
10
10
  <% end %>
11
11
 
12
- <h1><%= raw title %></h1>
12
+ <h1><%= title %></h1>
13
13
 
14
14
  <%= yield %>
15
15
  </div>
@@ -9,6 +9,7 @@ unless has_children
9
9
  class: ['nav-link position-relative', (active ? 'active' : 'link-dark')].join(' '),
10
10
  }
11
11
  link_to_options[:target] = '_blank' if external
12
+ link_to_options[:aria] = { current: 'page' } if active
12
13
  end
13
14
 
14
15
  %>
@@ -28,7 +29,7 @@ end
28
29
  <ul class="list-unstyled mt-2 border-start border-1 ps-2">
29
30
  <% children.each do |child| %>
30
31
  <li class="py-1">
31
- <%= link_to child[:path], class: ['nav-link py-1', (active == child[:key] ? 'active' : 'link-dark')].join(' ') do %>
32
+ <%= link_to child[:path], class: ['nav-link py-1', (active == child[:key] ? 'active' : 'link-dark')].join(' '), aria: { current: ('page' if active == child[:key]) } do %>
32
33
  <%= child[:label] %>
33
34
  <% end %>
34
35
  </li>
@@ -1,4 +1,4 @@
1
- <% if I18n.locale == 'en' %>
1
+ <% if I18n.locale.to_s != 'it' %>
2
2
 
3
3
  You have received an invitation to use <%= Lato.config.application_title %>.
4
4
  <br><br>
@@ -1,4 +1,4 @@
1
- <% if I18n.locale == 'en' %>
1
+ <% if I18n.locale.to_s != 'it' %>
2
2
 
3
3
  To have access to all the features of <%= Lato.config.application_title %> you must confirm your email address.
4
4
  <br><br>
@@ -1,4 +1,4 @@
1
- <% if I18n.locale == 'en' %>
1
+ <% if I18n.locale.to_s != 'it' %>
2
2
 
3
3
  An update to your <%= Lato.config.application_title %> account password has been requested.
4
4
  <br><br>
@@ -1,4 +1,4 @@
1
- <% if I18n.locale == 'en' %>
1
+ <% if I18n.locale.to_s != 'it' %>
2
2
 
3
3
  An access to your <b><%= Lato.config.application_title %></b> account has been made with the IP address <b><%= @ip_address %></b>.
4
4
  <br><br>
@@ -1,11 +1,11 @@
1
1
  <% 10.times do %>
2
2
 
3
- <div class="modal fade" data-lato-action-target="modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
3
+ <div class="modal fade" data-lato-action-target="modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
4
4
  <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" data-lato-action-target="modalDialog">
5
5
  <div class="modal-content">
6
6
  <div class="modal-header">
7
7
  <h2 class="mb-0 mt-0 fs-4" data-lato-action-target="modalTitle"></h2>
8
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
8
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<%= I18n.t('lato.close') %>"></button>
9
9
  </div>
10
10
  <div class="modal-body" data-lato-action-target="modalBody">
11
11
  ...
@@ -2,7 +2,7 @@
2
2
  class="aside-opener shadow"
3
3
  data-controller="lato-aside-opener"
4
4
  data-action="click->lato-aside-opener#onClickToggle"
5
- aria-label="Toggle sidebar"
5
+ aria-label="<%= I18n.t('lato.toggle_sidebar') %>"
6
6
  >
7
- <i class="bi bi-layout-text-sidebar-reverse"></i>
7
+ <i class="bi bi-layout-text-sidebar-reverse" aria-hidden="true"></i>
8
8
  </button>
@@ -1,26 +1,24 @@
1
- <div class="lato-confirm modal fade" data-controller="lato-confirm" tabindex="-1" aria-hidden="true" data-bs-keyboard="false">
1
+ <div class="lato-confirm modal fade" data-controller="lato-confirm" tabindex="-1" aria-hidden="true" aria-labelledby="latoConfirmTitle">
2
2
  <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
3
3
  <div class="modal-content">
4
4
  <div class="modal-header">
5
- <h2 class="mb-0 mt-0 fs-4"><%= I18n.t('lato.confirm_title') %></h2>
6
- <button type="button" class="btn-close" data-action="click->lato-confirm#confirmCancel" aria-label="Close"></button>
5
+ <h2 class="mb-0 mt-0 fs-4" id="latoConfirmTitle"><%= I18n.t('lato.confirm_title') %></h2>
6
+ <button type="button" class="btn-close" data-action="click->lato-confirm#confirmCancel" aria-label="<%= I18n.t('lato.close') %>"></button>
7
7
  </div>
8
8
  <div class="modal-body" data-lato-confirm-target="modalBody">
9
9
  </div>
10
10
  <div class="modal-footer">
11
11
  <button
12
12
  type="button"
13
- class="btn btn-danger"
13
+ class="btn btn-secondary"
14
14
  data-action="click->lato-confirm#confirmCancel"
15
- aria-label="<%= I18n.t('lato.confirm_no') %>"
16
15
  ><%= I18n.t('lato.confirm_no') %></button>
17
16
  <button
18
17
  type="button"
19
- class="btn btn-success"
18
+ class="btn btn-primary"
20
19
  data-action="click->lato-confirm#confirmSuccess"
21
- aria-label="<%= I18n.t('lato.confirm_yes') %>"
22
20
  ><%= I18n.t('lato.confirm_yes') %></button>
23
21
  </div>
24
22
  </div>
25
23
  </div>
26
- </div>
24
+ </div>
@@ -1,11 +1,11 @@
1
1
  <div class="toast-container">
2
2
  <% if notice %>
3
3
  <div class="position-fixed end-0 bottom-0 p-3" style="z-index: 11;">
4
- <div data-controller="lato-feedback" id="feedbackNotice" class="toast hide" role="alert" aria-live="assertive" aria-atomic="true">
4
+ <div data-controller="lato-feedback" id="feedbackNotice" class="toast hide" role="status" aria-live="polite" aria-atomic="true">
5
5
  <div class="toast-header">
6
- <i class="bi bi-check-circle-fill text-success me-2"></i>
7
- <strong class="me-auto"><%= I18n.t('lato.operation_completed') %></strong>
8
- <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
6
+ <i class="bi bi-check-circle-fill text-success me-2" aria-hidden="true"></i>
7
+ <strong class="me-auto"><%= I18n.t('lato.notice_title') %></strong>
8
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="<%= I18n.t('lato.close') %>"></button>
9
9
  </div>
10
10
  <div class="toast-body">
11
11
  <%= notice %>
@@ -16,11 +16,12 @@
16
16
 
17
17
  <% if alert %>
18
18
  <div class="position-fixed end-0 bottom-0 p-3" style="z-index: 11;">
19
- <div data-controller="lato-feedback" id="feedbackAlert" class="toast hide" role="alert" aria-live="assertive" aria-atomic="true">
19
+ <%# Errors stay visible until dismissed: auto hiding gives no time to read them. %>
20
+ <div data-controller="lato-feedback" id="feedbackAlert" class="toast hide" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="false">
20
21
  <div class="toast-header">
21
- <i class="bi bi-check-circle-fill text-danger me-2"></i>
22
- <strong class="me-auto"><%= I18n.t('lato.operation_failed') %></strong>
23
- <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
22
+ <i class="bi bi-exclamation-triangle-fill text-danger me-2" aria-hidden="true"></i>
23
+ <strong class="me-auto"><%= I18n.t('lato.alert_title') %></strong>
24
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="<%= I18n.t('lato.close') %>"></button>
24
25
  </div>
25
26
  <div class="toast-body">
26
27
  <%= alert %>
@@ -1,4 +1,4 @@
1
- <% if I18n.locale == 'en' %>
1
+ <% if I18n.locale.to_s != 'it' %>
2
2
  Hello,
3
3
  <br><br>
4
4
  <% else %>
@@ -1,4 +1,4 @@
1
- <div class="fixed-bottom p-3 d-none" data-controller="lato-network">
2
- <span class="badge rounded-pill text-bg-danger fs-6 d-none" data-lato-network-target="offline">You are offline</span>
3
- <span class="badge rounded-pill text-bg-success fs-6 d-none" data-lato-network-target="online">You are online</span>
1
+ <div class="fixed-bottom p-3 d-none" data-controller="lato-network" role="status" aria-live="polite">
2
+ <span class="badge rounded-pill text-bg-danger fs-6 d-none" data-lato-network-target="offline"><%= I18n.t('lato.network_offline') %></span>
3
+ <span class="badge rounded-pill text-bg-success fs-6 d-none" data-lato-network-target="online"><%= I18n.t('lato.network_online') %></span>
4
4
  </div>
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no,interactive-widget=resizes-content">
6
- <title><%= @layout_page_title %></title>
6
+ <title><%= @layout_page_title.presence || Lato.config.application_title %></title>
7
7
  <%= csrf_meta_tags %>
8
8
  <%= csp_meta_tag %>
9
9
 
@@ -27,6 +27,7 @@
27
27
  class="controller-<%= controller_name %> action-<%= action_name %> <%= @layout_body_classes.join(' ') %> <%= cookies[:lato_aside_hidden] == 'true' ? 'aside-hidden' : '' %>"
28
28
  data-controller="lato-action lato-guide"
29
29
  >
30
+ <a class="visually-hidden-focusable position-absolute top-0 start-0 p-2 bg-white" style="z-index: 2100;" href="#main"><%= I18n.t('lato.skip_to_content') %></a>
30
31
  <header>
31
32
  <%= render 'layouts/lato/navbar' %>
32
33
  </header>
@@ -39,7 +40,7 @@
39
40
  </aside>
40
41
  <% end %>
41
42
 
42
- <main class="<%= @layout_sidebar ? 'layout_sidebar' : '' %>">
43
+ <main id="main" tabindex="-1" class="<%= @layout_sidebar ? 'layout_sidebar' : '' %>">
43
44
  <%= render 'layouts/lato/content' %>
44
45
  </main>
45
46
  </div>
@@ -1,5 +1,28 @@
1
1
  en:
2
2
  lato:
3
+ close: "Close"
4
+ notice_title: "Notification"
5
+ alert_title: "Error"
6
+ sort_ascending: "Sort by %{column} ascending"
7
+ sort_descending: "Sort by %{column} descending"
8
+ password_requirements: "At least %{count} characters."
9
+ too_many_requests: "Too many requests, please wait %{minutes} minutes to retry."
10
+ hcaptcha_missing: "Please complete the captcha verification."
11
+ hcaptcha_failed: "Captcha verification failed, please retry."
12
+ security_confirmation_required: "Confirm your current password and enabled second factor to continue."
13
+ current_password: "Current password"
14
+ current_authenticator_code: "Current authenticator code"
15
+ security_key_confirmation_help: "Confirm with your registered security key when submitting."
16
+ confirm_authenticator_code: "Confirm authenticator code"
17
+ authenticator_qr_code_alt: "Authenticator enrollment QR code"
18
+ authenticator_manual_key: "Or enter this key manually in your authenticator app"
19
+ connecting: "Connecting..."
20
+ back_to_account: "Back to your account"
21
+ network_offline: "You are offline"
22
+ network_online: "You are online"
23
+ toggle_sidebar: "Toggle sidebar"
24
+ skip_to_content: "Skip to main content"
25
+ copied: "Copied to clipboard"
3
26
  account: Account
4
27
  signout: Logout
5
28
  signin: Login
@@ -144,6 +167,7 @@ en:
144
167
  credentials_not_correct: Email or password not correct
145
168
  privacy_policy_invalid: To accept the privacy policy you must select the confirmation checkbox
146
169
  terms_and_conditions_invalid: To accept the terms and conditions you must select the confirmation checkbox
170
+ invitation_invalid: Invalid invitation
147
171
  web3_address_invalid: The address you send is not corretly signed
148
172
  web3_connection_error: Impossible to connect the wallet
149
173
  authenticator_code_invalid: The code you inserted is not correct
@@ -153,6 +177,8 @@ en:
153
177
  webauthn_authentication_failed: Passkey authentication failed. Please try again or use another method.
154
178
  password:
155
179
  not_correct: not correct
180
+ password_confirmation:
181
+ confirmation: does not match the password
156
182
  email:
157
183
  taken: already used with another account
158
184
  not_registered: not registered
@@ -163,8 +189,14 @@ en:
163
189
  inclusion: not accepted
164
190
  web3_address:
165
191
  not_correct: not correct
192
+ lato/operation:
193
+ attributes:
194
+ base:
195
+ start_error: "Unable to start the job"
166
196
  lato/invitation:
167
197
  attributes:
198
+ email:
199
+ taken: "already used by another user"
168
200
  base:
169
201
  already_accepted: Invitation already accepted
170
202
  email_sending_error: Unable to send mail
@@ -2,6 +2,29 @@
2
2
 
3
3
  fr:
4
4
  lato:
5
+ close: "Fermer"
6
+ notice_title: "Notification"
7
+ alert_title: "Erreur"
8
+ sort_ascending: "Trier par %{column} croissant"
9
+ sort_descending: "Trier par %{column} d\u00E9croissant"
10
+ password_requirements: "Au moins %{count} caract\u00E8res."
11
+ too_many_requests: "Trop de requ\u00EAtes, veuillez attendre %{minutes} minutes avant de r\u00E9essayer."
12
+ hcaptcha_missing: "Veuillez compl\u00E9ter la v\u00E9rification captcha."
13
+ hcaptcha_failed: "La v\u00E9rification captcha a \u00E9chou\u00E9, veuillez r\u00E9essayer."
14
+ security_confirmation_required: "Confirmez votre mot de passe actuel et le second facteur activ\u00E9 pour continuer."
15
+ current_password: "Mot de passe actuel"
16
+ current_authenticator_code: "Code authenticator actuel"
17
+ security_key_confirmation_help: "Confirmez avec votre cl\u00E9 de s\u00E9curit\u00E9 enregistr\u00E9e lors de l'envoi."
18
+ confirm_authenticator_code: "Confirmer le code authenticator"
19
+ authenticator_qr_code_alt: "QR code de configuration de l'authenticator"
20
+ authenticator_manual_key: "Ou saisissez cette cl\u00E9 manuellement dans votre application authenticator"
21
+ connecting: "Connexion en cours..."
22
+ back_to_account: "Retour \u00E0 votre compte"
23
+ network_offline: "Vous \u00EAtes hors ligne"
24
+ network_online: "Vous \u00EAtes en ligne"
25
+ toggle_sidebar: "Afficher/masquer la barre lat\u00E9rale"
26
+ skip_to_content: "Aller au contenu principal"
27
+ copied: "Copi\u00E9 dans le presse-papiers"
5
28
  account: Compte
6
29
  signout: Déconnexion
7
30
  signin: Connexion
@@ -173,8 +196,14 @@ fr:
173
196
  inclusion: non acceptées
174
197
  web3_address:
175
198
  not_correct: incorrect
199
+ lato/operation:
200
+ attributes:
201
+ base:
202
+ start_error: "Impossible de lancer le job"
176
203
  lato/invitation:
177
204
  attributes:
205
+ email:
206
+ taken: "d\u00E9j\u00E0 utilis\u00E9e par un autre utilisateur"
178
207
  base:
179
208
  already_accepted: Invitation déjà acceptée par l'utilisateur
180
209
  email_sending_error: Impossible d'envoyer l'e-mail
@@ -2,6 +2,29 @@
2
2
 
3
3
  it:
4
4
  lato:
5
+ close: "Chiudi"
6
+ notice_title: "Notifica"
7
+ alert_title: "Errore"
8
+ sort_ascending: "Ordina per %{column} crescente"
9
+ sort_descending: "Ordina per %{column} decrescente"
10
+ password_requirements: "Almeno %{count} caratteri."
11
+ too_many_requests: "Troppe richieste, attendi %{minutes} minuti prima di riprovare."
12
+ hcaptcha_missing: "Completa la verifica captcha."
13
+ hcaptcha_failed: "Verifica captcha non riuscita, riprova."
14
+ security_confirmation_required: "Conferma la password attuale e il secondo fattore attivo per continuare."
15
+ current_password: "Password attuale"
16
+ current_authenticator_code: "Codice authenticator attuale"
17
+ security_key_confirmation_help: "Conferma con la tua chiave di sicurezza registrata al momento dell'invio."
18
+ confirm_authenticator_code: "Conferma codice authenticator"
19
+ authenticator_qr_code_alt: "Codice QR per configurare l'authenticator"
20
+ authenticator_manual_key: "Oppure inserisci manualmente questa chiave nella tua app authenticator"
21
+ connecting: "Connessione in corso..."
22
+ back_to_account: "Torna al tuo account"
23
+ network_offline: "Sei offline"
24
+ network_online: "Sei online"
25
+ toggle_sidebar: "Mostra/nascondi barra laterale"
26
+ skip_to_content: "Vai al contenuto principale"
27
+ copied: "Copiato negli appunti"
5
28
  account: Account
6
29
  signout: Esci
7
30
  signin: Accedi
@@ -173,8 +196,14 @@ it:
173
196
  inclusion: non accettati
174
197
  web3_address:
175
198
  not_correct: non corretto
199
+ lato/operation:
200
+ attributes:
201
+ base:
202
+ start_error: "Impossibile eseguire il job"
176
203
  lato/invitation:
177
204
  attributes:
205
+ email:
206
+ taken: "gi\u00E0 utilizzata da un altro utente"
178
207
  base:
179
208
  already_accepted: Invito già accettato dall'utente
180
209
  email_sending_error: Impossibile inviare mail
@@ -2,6 +2,29 @@
2
2
 
3
3
  ro:
4
4
  lato:
5
+ close: "\u00CEnchide"
6
+ notice_title: "Notificare"
7
+ alert_title: "Eroare"
8
+ sort_ascending: "Sorteaz\u0103 dup\u0103 %{column} cresc\u0103tor"
9
+ sort_descending: "Sorteaz\u0103 dup\u0103 %{column} descresc\u0103tor"
10
+ password_requirements: "Cel pu\u021Bin %{count} caractere."
11
+ too_many_requests: "Prea multe cereri, a\u0219tepta\u021Bi %{minutes} minute \u00EEnainte de a re\u00EEncerca."
12
+ hcaptcha_missing: "Completa\u021Bi verificarea captcha."
13
+ hcaptcha_failed: "Verificarea captcha a e\u0219uat, re\u00EEncerca\u021Bi."
14
+ security_confirmation_required: "Confirma\u021Bi parola curent\u0103 \u0219i al doilea factor activ pentru a continua."
15
+ current_password: "Parola curent\u0103"
16
+ current_authenticator_code: "Codul authenticator curent"
17
+ security_key_confirmation_help: "Confirma\u021Bi cu cheia de securitate \u00EEnregistrat\u0103 la trimitere."
18
+ confirm_authenticator_code: "Confirma\u021Bi codul authenticator"
19
+ authenticator_qr_code_alt: "Cod QR pentru configurarea authenticator"
20
+ authenticator_manual_key: "Sau introduce\u021Bi manual aceast\u0103 cheie \u00EEn aplica\u021Bia authenticator"
21
+ connecting: "Se conecteaz\u0103..."
22
+ back_to_account: "\u00CEnapoi la contul t\u0103u"
23
+ network_offline: "Sunte\u021Bi offline"
24
+ network_online: "Sunte\u021Bi online"
25
+ toggle_sidebar: "Afi\u0219eaz\u0103/ascunde bara lateral\u0103"
26
+ skip_to_content: "Salt la con\u021Binutul principal"
27
+ copied: "Copiat \u00EEn clipboard"
5
28
  account: Cont
6
29
  signout: Deconectare
7
30
  signin: Conectare
@@ -173,8 +196,14 @@ ro:
173
196
  inclusion: neacceptați
174
197
  web3_address:
175
198
  not_correct: incorect
199
+ lato/operation:
200
+ attributes:
201
+ base:
202
+ start_error: "Imposibil de pornit jobul"
176
203
  lato/invitation:
177
204
  attributes:
205
+ email:
206
+ taken: "deja folosit de alt utilizator"
178
207
  base:
179
208
  already_accepted: Invitația deja acceptată de utilizator
180
209
  email_sending_error: Imposibil să se trimită email-ul
data/config/routes.rb CHANGED
@@ -40,6 +40,7 @@ Lato::Engine.routes.draw do
40
40
 
41
41
  scope :account do
42
42
  get '', to: 'account#index', as: :account
43
+ post 'security_challenge', to: 'account#security_challenge', as: :account_security_challenge
43
44
  patch 'update_user_action', to: 'account#update_user_action', as: :account_update_user_action
44
45
  patch 'update_authenticator_action', to: 'account#update_authenticator_action', as: :account_update_authenticator_action
45
46
  patch 'update_webauthn_action', to: 'account#update_webauthn_action', as: :account_update_webauthn_action
@@ -0,0 +1,16 @@
1
+ class CreateLatoSessionRecords < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :lato_session_records do |t|
4
+ t.references :lato_user, null: false, foreign_key: true
5
+ t.string :token_digest, null: false
6
+ t.datetime :expires_at, null: false
7
+ t.string :security_grant_action
8
+ t.datetime :security_grant_expires_at
9
+ t.string :security_challenge
10
+ t.datetime :security_challenge_expires_at
11
+ t.timestamps
12
+ end
13
+ add_index :lato_session_records, :token_digest, unique: true
14
+ add_index :lato_session_records, :expires_at
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ class AddSecondFactorCountersToLatoUsers < ActiveRecord::Migration[7.1]
2
+ def change
3
+ add_column :lato_users, :authenticator_last_used_at, :datetime
4
+ add_column :lato_users, :webauthn_sign_count, :integer, null: false, default: 0
5
+ end
6
+ end
data/lib/lato/config.rb CHANGED
@@ -15,6 +15,9 @@ module Lato
15
15
  # Max password recovery emails sent per hour across all users (nil disables the cap)
16
16
  attr_accessor :auth_recover_password_hourly_limit
17
17
 
18
+ # Minimum password length, checked whenever a password is set
19
+ attr_accessor :auth_password_min_length
20
+
18
21
  # Hcaptcha configs
19
22
  attr_accessor :hcaptcha_site_key, :hcaptcha_secret
20
23
 
@@ -53,6 +56,7 @@ module Lato
53
56
  @auth_disable_webauthn = false
54
57
  @auth_disable_signin_success_mail = false
55
58
  @auth_recover_password_hourly_limit = 100
59
+ @auth_password_min_length = 8
56
60
 
57
61
  @hcaptcha_site_key = nil
58
62
  @hcaptcha_secret = nil
data/lib/lato/engine.rb CHANGED
@@ -12,7 +12,16 @@ module Lato
12
12
  app.config.assets.precompile << "lato_manifest.js"
13
13
  end
14
14
 
15
+ initializer "lato.filter_credentials" do |app|
16
+ app.config.filter_parameters += %i[current_password current_authenticator_code current_webauthn_credential authenticator_code authenticator_secret webauthn_credential web3_signed_nonce code accepted_code]
17
+ end
18
+
15
19
  config.after_initialize do
20
+ # Recovery codes, verification codes and rate limits live in Rails.cache: a null store silently disables them.
21
+ if Rails.env.production? && Rails.cache.is_a?(ActiveSupport::Cache::NullStore)
22
+ Rails.logger.warn("[Lato] Rails.cache is a NullStore: password recovery, email verification and rate limiting will not work. Configure a shared cache store (Redis, Solid Cache, Memcache).")
23
+ end
24
+
16
25
  if Lato.config.webauthn_connection
17
26
  WebAuthn.configure do |config|
18
27
  config.allowed_origins = Lato.config.webauthn_origin.is_a?(Array) ? Lato.config.webauthn_origin : [Lato.config.webauthn_origin]
data/lib/lato/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Lato
2
- VERSION = "3.24.0"
2
+ VERSION = "3.26.0"
3
3
  end