lato 3.26.0 → 3.27.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -32
  3. data/app/assets/config/lato_manifest.js +3 -1
  4. data/app/assets/fonts/lato/bootstrap-icons.woff +0 -0
  5. data/app/assets/fonts/lato/bootstrap-icons.woff2 +0 -0
  6. data/app/assets/javascripts/lato/controllers/lato_account_webauthn_controller.js +1 -1
  7. data/app/assets/javascripts/lato/controllers/lato_auto_submit_controller.js +8 -0
  8. data/app/assets/javascripts/lato/controllers/lato_input_autocomplete2_controller.js +2 -2
  9. data/app/assets/javascripts/lato/controllers/lato_input_autocomplete_controller.js +2 -2
  10. data/app/assets/javascripts/lato/controllers/lato_web3_sign_controller.js +30 -0
  11. data/app/assets/javascripts/lato/debounce.js +8 -0
  12. data/app/assets/javascripts/lato/vendor/ethers.esm.min.js +1 -0
  13. data/app/assets/stylesheets/lato/_bootstrap-icons.scss +2079 -0
  14. data/app/assets/stylesheets/lato/application.scss +18 -1
  15. data/app/controllers/concerns/lato/sessionable.rb +1 -0
  16. data/app/controllers/lato/authentication_controller.rb +1 -0
  17. data/app/helpers/lato/components_helper.rb +10 -1
  18. data/app/models/lato/invitation.rb +9 -0
  19. data/app/models/lato/session.rb +30 -1
  20. data/app/models/lato/user.rb +20 -4
  21. data/app/views/lato/account/_form-web3.html.erb +8 -37
  22. data/app/views/lato/account/_form-webauthn.html.erb +3 -3
  23. data/app/views/lato/account/_security-confirmation.html.erb +7 -4
  24. data/app/views/lato/account/index.html.erb +1 -1
  25. data/app/views/lato/authentication/_form-verify-email.html.erb +1 -2
  26. data/app/views/lato/authentication/_form-web3-signin.html.erb +6 -35
  27. data/app/views/lato/components/_form_item_input_signature.html.erb +1 -1
  28. data/app/views/lato/components/_index.html.erb +2 -2
  29. data/app/views/lato/components/_operation.html.erb +2 -2
  30. data/app/views/layouts/lato/application.html.erb +2 -2
  31. data/config/importmap.rb +4 -3
  32. data/config/locales/en.yml +2 -0
  33. data/config/locales/fr.yml +2 -0
  34. data/config/locales/it.yml +2 -0
  35. data/config/locales/ro.yml +2 -0
  36. data/lib/lato/config.rb +19 -0
  37. data/lib/lato/version.rb +1 -1
  38. metadata +8 -1
@@ -20,7 +20,7 @@ $gray-900: #1f262c !default;
20
20
  $tooltip-opacity: 1;
21
21
  $tooltip-max-width: 300px;
22
22
 
23
- @import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css");
23
+ @import "bootstrap-icons";
24
24
  @import "bootstrap";
25
25
 
26
26
  /** Turbo loading **/
@@ -38,6 +38,20 @@ $tooltip-max-width: 300px;
38
38
  margin: 0 4px;
39
39
  }
40
40
 
41
+ // WCAG 2.2 target size (2.5.8): interactive targets of at least 24x24px.
42
+ .pagination > span > a {
43
+ display: inline-flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ min-width: 24px;
47
+ min-height: 24px;
48
+ }
49
+
50
+ // Bootstrap's pink code color fails contrast on tinted alert backgrounds.
51
+ .alert code {
52
+ color: inherit;
53
+ }
54
+
41
55
  .pagination .current {
42
56
  font-weight: bold;
43
57
  }
@@ -143,6 +157,9 @@ main, aside {
143
157
  .btn {
144
158
  font-size: 12px;
145
159
  padding: 0.15rem 0.3rem;
160
+ // 25px: btn-group overlaps adjacent buttons by 1px, the visible target must stay >= 24px (WCAG 2.5.8)
161
+ min-width: 25px;
162
+ min-height: 24px;
146
163
  }
147
164
  }
148
165
 
@@ -5,6 +5,7 @@ module Lato
5
5
  included do
6
6
  before_action do
7
7
  @session = Lato::Session.new(cookies.encrypted[:lato_session])
8
+ @session.track_activity
8
9
  end
9
10
  end
10
11
 
@@ -188,6 +188,7 @@ module Lato
188
188
 
189
189
  def accept_invitation
190
190
  @user = Lato::User.new(email: @invitation.email)
191
+ @user.errors.add(:base, :invitation_expired) if @invitation.expired?
191
192
  end
192
193
 
193
194
  def accept_invitation_action
@@ -284,11 +284,20 @@ module Lato
284
284
  form.submit label, options
285
285
  end
286
286
 
287
+ # Kaminari labels every paginator "pager": several index tables on one page would expose
288
+ # identical navigation landmarks, so each one gets a label from its index key (unique per page).
289
+ def lato_index_paginate(collection, key, **options)
290
+ name = key.to_s == 'default' ? collection.model.model_name.human(count: 2) : key.to_s.humanize
291
+ label = I18n.t('lato.pagination_label', name: name)
292
+ paginate(collection, param_name: "#{key}_page", **options)
293
+ .to_str.sub('aria-label="pager"', "aria-label=\"#{ERB::Util.html_escape(label)}\"").html_safe
294
+ end
295
+
287
296
  # Data
288
297
  ##
289
298
 
290
299
  def lato_data_badge(label, color = 'primary')
291
- content_tag :span, label, class: "badge rounded-pill bg-#{color}"
300
+ content_tag :span, label, class: "badge rounded-pill text-bg-#{color}"
292
301
  end
293
302
 
294
303
  def lato_data_user(label, image_url = nil)
@@ -50,6 +50,14 @@ module Lato
50
50
  !!accepted_at
51
51
  end
52
52
 
53
+ # The lifetime counts from the last time the invitation email was sent (send_invite touches the record).
54
+ def expired?
55
+ lifetime = Lato.config.invitation_lifetime
56
+ return false if lifetime.blank? || accepted?
57
+
58
+ updated_at.present? && updated_at <= lifetime.ago
59
+ end
60
+
53
61
  # Operations
54
62
  ##
55
63
 
@@ -71,6 +79,7 @@ module Lato
71
79
  end
72
80
 
73
81
  c_email_invite_semaphore(true)
82
+ touch
74
83
 
75
84
  true
76
85
  end
@@ -69,6 +69,21 @@ module Lato
69
69
  end
70
70
  end
71
71
 
72
+ # Activity is written at most once per interval to avoid a database write on every request.
73
+ ACTIVITY_TOUCH_INTERVAL = 1.minute
74
+
75
+ # Revokes the session when idle for longer than Lato.config.session_idle_timeout, otherwise records the activity.
76
+ def track_activity
77
+ timeout = Lato.config.session_idle_timeout
78
+ return unless timeout && (current = unexpired_record)
79
+
80
+ if current.updated_at <= timeout.ago
81
+ current.destroy!
82
+ elsif current.updated_at <= [ACTIVITY_TOUCH_INTERVAL, timeout / 2].min.ago
83
+ current.touch
84
+ end
85
+ end
86
+
72
87
  def destroy
73
88
  record&.destroy!
74
89
  @session = nil
@@ -88,9 +103,23 @@ module Lato
88
103
 
89
104
  # Always query revocation state, including when a Session instance was already used.
90
105
  def record
106
+ scope = unexpired_record_scope
107
+ return unless scope
108
+
109
+ timeout = Lato.config.session_idle_timeout
110
+ scope = scope.where("updated_at > ?", timeout.ago) if timeout
111
+ scope.first
112
+ end
113
+
114
+ # Not expired record, ignoring the idle timeout.
115
+ def unexpired_record
116
+ unexpired_record_scope&.first
117
+ end
118
+
119
+ def unexpired_record_scope
91
120
  return unless @session&.dig(:token).is_a?(String) && @session[:token].present?
92
121
 
93
- Lato::SessionRecord.where(lato_user_id: @session[:user_id], token_digest: Digest::SHA256.hexdigest(@session[:token])).where("expires_at > ?", Time.current).first
122
+ Lato::SessionRecord.where(lato_user_id: @session[:user_id], token_digest: Digest::SHA256.hexdigest(@session[:token])).where("expires_at > ?", Time.current)
94
123
  end
95
124
 
96
125
  def parse_session(session)
@@ -7,6 +7,9 @@ module Lato
7
7
 
8
8
  has_secure_password
9
9
 
10
+ # Evaluated when the class loads: Lato.config.encrypt_authenticator_secret must be set in an initializer.
11
+ encrypts :authenticator_secret if Lato.config.encrypt_authenticator_secret
12
+
10
13
  # Validations
11
14
  ##
12
15
 
@@ -333,6 +336,11 @@ module Lato
333
336
  return
334
337
  end
335
338
 
339
+ if invitation.expired?
340
+ errors.add(:base, :invitation_expired)
341
+ return
342
+ end
343
+
336
344
  ActiveRecord::Base.transaction do
337
345
  raise ActiveRecord::Rollback unless save && invitation.update(
338
346
  accepted_at: Time.now,
@@ -376,13 +384,15 @@ module Lato
376
384
  name: email,
377
385
  display_name: full_name
378
386
  },
379
- exclude: webauthn_exclude_credentials.map { |cred| Base64.strict_encode64(cred) }
387
+ exclude: webauthn_exclude_credentials.map { |cred| Base64.strict_encode64(cred) },
388
+ authenticator_selection: { user_verification: Lato.config.webauthn_user_verification }
380
389
  )
381
390
  end
382
391
 
383
392
  def webauthn_authentication_options
384
393
  WebAuthn::Credential.options_for_get(
385
- allow: webauthn_allow_credentials.map { |cred| Base64.strict_encode64(cred) }
394
+ allow: webauthn_allow_credentials.map { |cred| Base64.strict_encode64(cred) },
395
+ user_verification: Lato.config.webauthn_user_verification
386
396
  )
387
397
  end
388
398
 
@@ -399,7 +409,7 @@ module Lato
399
409
 
400
410
  parsed_payload = JSON.parse(credential_payload)
401
411
  credential = WebAuthn::Credential.from_create(parsed_payload)
402
- credential.verify(Base64.strict_decode64(encoded_challenge))
412
+ credential.verify(Base64.strict_decode64(encoded_challenge), user_verification: webauthn_user_verification_required?)
403
413
 
404
414
  update(
405
415
  webauthn_id: Base64.strict_encode64(credential.raw_id),
@@ -440,7 +450,8 @@ module Lato
440
450
  credential.verify(
441
451
  encoded_challenge,
442
452
  public_key: webauthn_public_key,
443
- sign_count: webauthn_sign_count.to_i
453
+ sign_count: webauthn_sign_count.to_i,
454
+ user_verification: webauthn_user_verification_required?
444
455
  )
445
456
  update_column(:webauthn_sign_count, credential.sign_count.to_i)
446
457
 
@@ -563,6 +574,11 @@ module Lato
563
574
  )
564
575
  end
565
576
 
577
+ # The client honours the requested level only as a hint: "required" must also be checked on the response.
578
+ def webauthn_user_verification_required?
579
+ Lato.config.webauthn_user_verification.to_s == 'required'
580
+ end
581
+
566
582
  def webauthn_user_handle
567
583
  Digest::SHA256.digest("lato-user-#{id}")
568
584
  end
@@ -30,50 +30,21 @@ user ||= Lato::User.new
30
30
  </div>
31
31
  <% elsif session[:web3_nonce] %>
32
32
  <div class="alert alert-light mb-0">
33
- <h4 class="alert-heading" role="status"><%= I18n.t('lato.connecting') %></h4>
33
+ <h3 class="alert-heading h4" role="status"><%= I18n.t('lato.connecting') %></h3>
34
34
  <div class="progress" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100">
35
35
  <div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 100%"></div>
36
36
  </div>
37
37
  </div>
38
38
 
39
- <span id="account_form-web3__nonce" style="display: none;"><%= session[:web3_nonce] %></span>
40
- <%= form.hidden_field :web3_address, id: 'account_form-web3__input-web3_address' %>
41
- <%= form.hidden_field :web3_signed_nonce, id: 'account_form-web3__input-web3_signed_nonce' %>
42
- <%= lato_form_submit form, I18n.t('lato.confirm'), class: %w[btn-primary d-none], id: 'account_form-web3__submit' %>
43
-
44
- <script>
45
- (async () => {
46
- // Pinned exact version: dynamic import() can not carry an SRI hash (see README security checklist).
47
- const ethers = await import('https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.esm.min.js')
48
-
49
- const nonce = document.getElementById('account_form-web3__nonce').textContent
50
- const inputAddress = document.getElementById('account_form-web3__input-web3_address')
51
- const inputSignedNonce = document.getElementById('account_form-web3__input-web3_signed_nonce')
52
- const submitButton = document.getElementById('account_form-web3__submit')
53
-
54
- let address = ''
55
- let signedNonce = ''
56
-
57
- try {
58
- if (window.ethereum) {
59
- const provider = new ethers.ethers.providers.Web3Provider(window.ethereum)
60
- await provider.send('eth_requestAccounts', [])
61
- const signer = provider.getSigner()
62
- address = await signer.getAddress()
63
- signedNonce = await signer.signMessage(nonce)
64
- }
65
- } catch (error) {
66
- console.error(error)
67
- }
68
-
69
- inputAddress.value = address
70
- inputSignedNonce.value = signedNonce
71
- submitButton.click()
72
- })()
73
- </script>
39
+ <div data-controller="lato-web3-sign">
40
+ <span id="account_form-web3__nonce" style="display: none;" data-lato-web3-sign-target="nonce"><%= session[:web3_nonce] %></span>
41
+ <%= form.hidden_field :web3_address, id: 'account_form-web3__input-web3_address', data: { lato_web3_sign_target: 'address' } %>
42
+ <%= form.hidden_field :web3_signed_nonce, id: 'account_form-web3__input-web3_signed_nonce', data: { lato_web3_sign_target: 'signedNonce' } %>
43
+ <%= lato_form_submit form, I18n.t('lato.confirm'), class: %w[btn-primary d-none], id: 'account_form-web3__submit', data: { lato_web3_sign_target: 'submit' } %>
44
+ </div>
74
45
  <% else %>
75
46
  <div class="alert alert-light mb-0">
76
- <h4 class="alert-heading"><%= I18n.t('lato.account_web3_start_title') %></h4>
47
+ <h3 class="alert-heading h4"><%= I18n.t('lato.account_web3_start_title') %></h3>
77
48
  <p>
78
49
  <%= raw I18n.t('lato.account_web3_start_description') %>
79
50
  </p>
@@ -20,7 +20,7 @@ end
20
20
 
21
21
  <% if user.webauthn_enabled? %>
22
22
  <div class="alert alert-success mb-3">
23
- <h4 class="alert-heading"><%= I18n.t('lato.account_webauthn_ready_title') %></h4>
23
+ <h3 class="alert-heading h4"><%= I18n.t('lato.account_webauthn_ready_title') %></h3>
24
24
  <p class="mb-0">
25
25
  <%= raw I18n.t('lato.account_webauthn_ready_description') %>
26
26
  </p>
@@ -36,7 +36,7 @@ end
36
36
  <%= form.hidden_field :webauthn_credential, data: { lato_account_webauthn_target: 'credentialInput' } %>
37
37
 
38
38
  <div class="alert alert-light mb-3" data-lato-account-webauthn-target="status">
39
- <h4 class="alert-heading"><%= I18n.t('lato.account_webauthn_in_progress_title') %></h4>
39
+ <h3 class="alert-heading h4"><%= I18n.t('lato.account_webauthn_in_progress_title') %></h3>
40
40
  <p class="mb-0">
41
41
  <%= raw I18n.t('lato.account_webauthn_in_progress_description') %>
42
42
  </p>
@@ -53,7 +53,7 @@ end
53
53
  </div>
54
54
  <% else %>
55
55
  <div class="alert alert-light mb-0">
56
- <h4 class="alert-heading"><%= I18n.t('lato.account_webauthn_start_title') %></h4>
56
+ <h3 class="alert-heading h4"><%= I18n.t('lato.account_webauthn_start_title') %></h3>
57
57
  <p>
58
58
  <%= raw I18n.t('lato.account_webauthn_start_description') %>
59
59
  </p>
@@ -1,9 +1,12 @@
1
+ <%# Rendered in several account forms on the same page: suffix ids so each label targets its own field. %>
2
+ <% password_id = form.field_id(:current_password, form.object_id) %>
3
+ <% code_id = form.field_id(:current_authenticator_code, form.object_id) %>
1
4
  <div class="mb-3" data-controller="lato-account-security" data-lato-account-security-challenge-url-value="<%= lato.account_security_challenge_path %>" data-lato-account-security-webauthn-value="<%= user.webauthn_enabled? && !user.authenticator_enabled? %>">
2
- <%= form.label :current_password, I18n.t('lato.current_password'), class: "form-label" %>
3
- <%= form.password_field :current_password, class: "form-control", autocomplete: "current-password" %>
5
+ <%= form.label :current_password, I18n.t('lato.current_password'), class: "form-label", for: password_id %>
6
+ <%= form.password_field :current_password, id: password_id, class: "form-control", autocomplete: "current-password" %>
4
7
  <% if user.authenticator_enabled? %>
5
- <%= form.label :current_authenticator_code, I18n.t('lato.current_authenticator_code'), class: "form-label mt-2" %>
6
- <%= form.text_field :current_authenticator_code, value: nil, class: "form-control", autocomplete: "one-time-code", inputmode: "numeric" %>
8
+ <%= form.label :current_authenticator_code, I18n.t('lato.current_authenticator_code'), class: "form-label mt-2", for: code_id %>
9
+ <%= form.text_field :current_authenticator_code, id: code_id, value: nil, class: "form-control", autocomplete: "one-time-code", inputmode: "numeric" %>
7
10
  <% elsif user.webauthn_enabled? %>
8
11
  <%= form.hidden_field :current_webauthn_credential, value: nil, data: { lato_account_security_target: "credential" } %>
9
12
  <p class="text-muted mt-2"><%= I18n.t('lato.security_key_confirmation_help') %></p>
@@ -60,7 +60,7 @@
60
60
  </div>
61
61
  <div class="card-body">
62
62
  <div class="alert alert-warning">
63
- <h4 class="alert-heading"><%= I18n.t('lato.warning') %></h4>
63
+ <h3 class="alert-heading h4"><%= I18n.t('lato.warning') %></h3>
64
64
  <p>
65
65
  <%= raw I18n.t('lato.account_delete_details') %>
66
66
  </p>
@@ -7,12 +7,11 @@ code ||= nil
7
7
 
8
8
 
9
9
  <%= turbo_frame_tag 'authentication_form_verify_email' do %>
10
- <%= form_with model: user, url: lato.authentication_verify_email_action_path(id: user.id), class: 'text-center', data: { turbo_frame: '_self' } do |form| %>
10
+ <%= form_with model: user, url: lato.authentication_verify_email_action_path(id: user.id), class: 'text-center', data: { turbo_frame: '_self', controller: code ? 'lato-auto-submit' : nil } do |form| %>
11
11
  <% if code %>
12
12
  <%= form.hidden_field :code, value: code %>
13
13
  <%# Visible fallback when JavaScript is disabled or blocked by CSP. %>
14
14
  <button type="submit" class="btn btn-primary"><%= I18n.t('lato.verify_email') %></button>
15
- <script>document.getElementById('authentication_form_verify_email').querySelector('button[type="submit"]').click()</script>
16
15
  <% else %>
17
16
  <%= lato_form_notices class: %w[mb-3], fixed: true %>
18
17
  <%= lato_form_errors user, class: %w[mb-3], fixed: true %>
@@ -18,41 +18,12 @@ user ||= Lato::User.new
18
18
  </div>
19
19
  </div>
20
20
 
21
- <span id="account_form-web3__nonce" style="display: none;"><%= session[:web3_nonce] %></span>
22
- <%= form.hidden_field :web3_address, id: 'account_form-web3__input-web3_address' %>
23
- <%= form.hidden_field :web3_signed_nonce, id: 'account_form-web3__input-web3_signed_nonce' %>
24
- <%= lato_form_submit form, I18n.t('lato.confirm'), class: %w[btn-primary d-none], id: 'account_form-web3__submit' %>
25
-
26
- <script>
27
- (async () => {
28
- // Pinned exact version: dynamic import() can not carry an SRI hash (see README security checklist).
29
- const ethers = await import('https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.esm.min.js')
30
-
31
- const nonce = document.getElementById('account_form-web3__nonce').textContent
32
- const inputAddress = document.getElementById('account_form-web3__input-web3_address')
33
- const inputSignedNonce = document.getElementById('account_form-web3__input-web3_signed_nonce')
34
- const submitButton = document.getElementById('account_form-web3__submit')
35
-
36
- let address = ''
37
- let signedNonce = ''
38
-
39
- try {
40
- if (window.ethereum) {
41
- const provider = new ethers.ethers.providers.Web3Provider(window.ethereum)
42
- await provider.send('eth_requestAccounts', [])
43
- const signer = provider.getSigner()
44
- address = await signer.getAddress()
45
- signedNonce = await signer.signMessage(nonce)
46
- }
47
- } catch (error) {
48
- console.error(error)
49
- }
50
-
51
- inputAddress.value = address
52
- inputSignedNonce.value = signedNonce
53
- submitButton.click()
54
- })()
55
- </script>
21
+ <div data-controller="lato-web3-sign">
22
+ <span id="account_form-web3__nonce" style="display: none;" data-lato-web3-sign-target="nonce"><%= session[:web3_nonce] %></span>
23
+ <%= form.hidden_field :web3_address, id: 'account_form-web3__input-web3_address', data: { lato_web3_sign_target: 'address' } %>
24
+ <%= form.hidden_field :web3_signed_nonce, id: 'account_form-web3__input-web3_signed_nonce', data: { lato_web3_sign_target: 'signedNonce' } %>
25
+ <%= lato_form_submit form, I18n.t('lato.confirm'), class: %w[btn-primary d-none], id: 'account_form-web3__submit', data: { lato_web3_sign_target: 'submit' } %>
26
+ </div>
56
27
 
57
28
  <% else %>
58
29
  <div class="text-center">
@@ -102,7 +102,7 @@ default_tab = existing_url ? 'current' : 'draw'
102
102
  <%# Clear button (solo nelle tab di editing) %>
103
103
  <div class="d-flex justify-content-end mt-2" data-lato-input-signature-target="clearRow">
104
104
  <button type="button"
105
- class="btn btn-outline-secondary btn-sm"
105
+ class="btn btn-outline-primary btn-sm"
106
106
  style="font-size: 0.75rem;"
107
107
  data-action="click->lato-input-signature#clearSignature">
108
108
  <i class="bi bi-eraser"></i> <%= I18n.t('lato.signature_clear') %>
@@ -99,7 +99,7 @@ total_pages = collection.empty? ? params["#{key}_page"]&.to_i : 999_999_999 if s
99
99
  <span class="text-muted"><%= collection_total %> <%= I18n.t('lato.total_results').downcase %></span>
100
100
  <% end %>
101
101
  <% if collection.respond_to?(:total_pages) %>
102
- <div class="mt-3"><%= paginate(collection, param_name: "#{key}_page", total_pages: total_pages, window: 1) %></div>
102
+ <div class="mt-3"><%= lato_index_paginate(collection, key, total_pages: total_pages, window: 1) %></div>
103
103
  <% end %>
104
104
  </div>
105
105
 
@@ -161,7 +161,7 @@ total_pages = collection.empty? ? params["#{key}_page"]&.to_i : 999_999_999 if s
161
161
  <td colspan="<%= columns.length %>">
162
162
  <div class="d-flex justify-content-between align-items-center">
163
163
  <% if collection.respond_to?(:total_pages) %>
164
- <div><%= paginate(collection, param_name: "#{key}_page", total_pages: total_pages) %></div>
164
+ <div><%= lato_index_paginate(collection, key, total_pages: total_pages) %></div>
165
165
  <% else %>
166
166
  <div></div>
167
167
  <% end %>
@@ -29,7 +29,7 @@
29
29
 
30
30
  <% if operation.completed_status? %>
31
31
  <div class="text-center">
32
- <div class="fs-1 text-success">
32
+ <div class="fs-1 text-success-emphasis">
33
33
  <i class="bi bi-check-circle"></i>
34
34
  </div>
35
35
  <h2><%= I18n.t('lato.operation_completed_title') %></h2>
@@ -40,7 +40,7 @@
40
40
  <% end %>
41
41
 
42
42
  <% if operation.output_message? %>
43
- <p class="text-success fw-bold">
43
+ <p class="text-success-emphasis fw-bold">
44
44
  <%= operation.output_message %>
45
45
  </p>
46
46
  <% end %>
@@ -17,8 +17,8 @@
17
17
 
18
18
  <%= render 'layouts/lato/head_content' %>
19
19
 
20
- <% # TEMPORARY FIX: Popper js not working using bootstrap-gem %>
21
- <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
20
+ <%# Popper UMD build from the popper_js gem (bootstrap dependency): Bootstrap reads it from window.Popper %>
21
+ <%= javascript_include_tag "popper" %>
22
22
 
23
23
  <%= stylesheet_link_tag Lato.config.assets_stylesheet_entry, media: "all" %>
24
24
  <%= javascript_importmap_tags Lato.config.assets_importmap_entry %>
data/config/importmap.rb CHANGED
@@ -4,6 +4,7 @@ pin_all_from Lato::Engine.root.join("app/assets/javascripts/lato/controllers"),
4
4
  pin "@rails/activestorage", to: "activestorage.esm.js", preload: true
5
5
 
6
6
  pin "bootstrap", to: "bootstrap.js", preload: true
7
- # pin "@popperjs/core", to: "popper.js", preload: true
8
- pin "local-time", to: "https://ga.jspm.io/npm:local-time@3.0.3/app/assets/javascripts/local-time.es2017-esm.js"
9
- pin "lodash", to: "https://ga.jspm.io/npm:lodash@4.17.21/lodash.js"
7
+ pin "local-time", to: "local-time.es2017-esm.js"
8
+ pin "lato/debounce", to: "lato/debounce.js"
9
+ # Vendored (verified against the cdnjs SRI hash) and loaded on demand by the web3 controller.
10
+ pin "ethers", to: "lato/vendor/ethers.esm.min.js", preload: false
@@ -5,6 +5,7 @@ en:
5
5
  alert_title: "Error"
6
6
  sort_ascending: "Sort by %{column} ascending"
7
7
  sort_descending: "Sort by %{column} descending"
8
+ pagination_label: "Pagination: %{name}"
8
9
  password_requirements: "At least %{count} characters."
9
10
  too_many_requests: "Too many requests, please wait %{minutes} minutes to retry."
10
11
  hcaptcha_missing: "Please complete the captcha verification."
@@ -168,6 +169,7 @@ en:
168
169
  privacy_policy_invalid: To accept the privacy policy you must select the confirmation checkbox
169
170
  terms_and_conditions_invalid: To accept the terms and conditions you must select the confirmation checkbox
170
171
  invitation_invalid: Invalid invitation
172
+ invitation_expired: Invitation expired, ask for a new one
171
173
  web3_address_invalid: The address you send is not corretly signed
172
174
  web3_connection_error: Impossible to connect the wallet
173
175
  authenticator_code_invalid: The code you inserted is not correct
@@ -7,6 +7,7 @@ fr:
7
7
  alert_title: "Erreur"
8
8
  sort_ascending: "Trier par %{column} croissant"
9
9
  sort_descending: "Trier par %{column} d\u00E9croissant"
10
+ pagination_label: "Pagination : %{name}"
10
11
  password_requirements: "Au moins %{count} caract\u00E8res."
11
12
  too_many_requests: "Trop de requ\u00EAtes, veuillez attendre %{minutes} minutes avant de r\u00E9essayer."
12
13
  hcaptcha_missing: "Veuillez compl\u00E9ter la v\u00E9rification captcha."
@@ -175,6 +176,7 @@ fr:
175
176
  privacy_policy_invalid: Pour accepter la politique de confidentialité, vous devez cocher la case de confirmation
176
177
  terms_and_conditions_invalid: Pour accepter les conditions générales, vous devez cocher la case de confirmation
177
178
  invitation_invalid: Invitation non valide
179
+ invitation_expired: Invitation expirée, demandez-en une nouvelle
178
180
  web3_address_invalid: L'adresse envoyée n'est pas correctement signée
179
181
  web3_connection_error: Impossible de connecter le portefeuille
180
182
  authenticator_code_invalid: Le code saisi n'est pas correct
@@ -7,6 +7,7 @@ it:
7
7
  alert_title: "Errore"
8
8
  sort_ascending: "Ordina per %{column} crescente"
9
9
  sort_descending: "Ordina per %{column} decrescente"
10
+ pagination_label: "Paginazione: %{name}"
10
11
  password_requirements: "Almeno %{count} caratteri."
11
12
  too_many_requests: "Troppe richieste, attendi %{minutes} minuti prima di riprovare."
12
13
  hcaptcha_missing: "Completa la verifica captcha."
@@ -175,6 +176,7 @@ it:
175
176
  privacy_policy_invalid: Per accettare la privacy policy devi selezionare la checkbox di conferma
176
177
  terms_and_conditions_invalid: Per accettare i termini e condizioni devi selezionare la checkbox di conferma
177
178
  invitation_invalid: Invito non valido
179
+ invitation_expired: Invito scaduto, richiedine uno nuovo
178
180
  web3_address_invalid: L'inidirizzo inviato non è correttamente firmato
179
181
  web3_connection_error: Impossibile connettere il wallet
180
182
  authenticator_code_invalid: Il codice inserito non è corretto
@@ -7,6 +7,7 @@ ro:
7
7
  alert_title: "Eroare"
8
8
  sort_ascending: "Sorteaz\u0103 dup\u0103 %{column} cresc\u0103tor"
9
9
  sort_descending: "Sorteaz\u0103 dup\u0103 %{column} descresc\u0103tor"
10
+ pagination_label: "Paginare: %{name}"
10
11
  password_requirements: "Cel pu\u021Bin %{count} caractere."
11
12
  too_many_requests: "Prea multe cereri, a\u0219tepta\u021Bi %{minutes} minute \u00EEnainte de a re\u00EEncerca."
12
13
  hcaptcha_missing: "Completa\u021Bi verificarea captcha."
@@ -175,6 +176,7 @@ ro:
175
176
  privacy_policy_invalid: Pentru a accepta politica de confidențialitate trebuie să bifezi căsuța de confirmare
176
177
  terms_and_conditions_invalid: Pentru a accepta termenii și condițiile trebuie să bifezi căsuța de confirmare
177
178
  invitation_invalid: Invitație nevalidă
179
+ invitation_expired: Invitație expirată, solicită una nouă
178
180
  web3_address_invalid: Adresa trimisă nu este semnată corect
179
181
  web3_connection_error: Imposibil să se conecteze portofelul
180
182
  authenticator_code_invalid: Codul introdus nu este corect
data/lib/lato/config.rb CHANGED
@@ -9,6 +9,9 @@ module Lato
9
9
  # Session configs
10
10
  attr_accessor :session_lifetime, :session_root_path
11
11
 
12
+ # Revoke sessions inactive for longer than this duration (nil disables the check)
13
+ attr_accessor :session_idle_timeout
14
+
12
15
  # Authentication configs
13
16
  attr_accessor :auth_disable_signup, :auth_disable_recover_password, :auth_disable_web3, :auth_disable_authenticator, :auth_disable_webauthn, :auth_disable_signin_success_mail
14
17
 
@@ -18,6 +21,9 @@ module Lato
18
21
  # Minimum password length, checked whenever a password is set
19
22
  attr_accessor :auth_password_min_length
20
23
 
24
+ # Invitations not accepted within this duration from the last invitation email expire (nil means they never expire)
25
+ attr_accessor :invitation_lifetime
26
+
21
27
  # Hcaptcha configs
22
28
  attr_accessor :hcaptcha_site_key, :hcaptcha_secret
23
29
 
@@ -37,6 +43,13 @@ module Lato
37
43
  # Webauth connection
38
44
  attr_accessor :webauthn_connection, :webauthn_origin, :webauthn_rp_name
39
45
 
46
+ # WebAuthn user verification: "discouraged", "preferred" or "required" (enforced server-side)
47
+ attr_accessor :webauthn_user_verification
48
+
49
+ # Encrypt Lato::User#authenticator_secret with Active Record encryption.
50
+ # Read when Lato::User is loaded: set it in an initializer.
51
+ attr_accessor :encrypt_authenticator_secret
52
+
40
53
  # Web3 connection
41
54
  # NOTE: It requires the gem 'eth' to be installed in the application Gemfile
42
55
  attr_accessor :web3_connection
@@ -66,6 +79,9 @@ module Lato
66
79
 
67
80
  @session_lifetime = 30.days
68
81
  @session_root_path = nil # :tutorial_path
82
+ @session_idle_timeout = nil
83
+
84
+ @invitation_lifetime = 30.days
69
85
 
70
86
  @email_from = 'lato@example.com'
71
87
 
@@ -79,6 +95,9 @@ module Lato
79
95
  @webauthn_connection = false
80
96
  @webauthn_origin = 'http://localhost:3000'
81
97
  @webauthn_rp_name = 'Lato Application'
98
+ @webauthn_user_verification = 'preferred'
99
+
100
+ @encrypt_authenticator_secret = false
82
101
  end
83
102
  end
84
103
  end
data/lib/lato/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Lato
2
- VERSION = "3.26.0"
2
+ VERSION = "3.27.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lato
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.26.0
4
+ version: 3.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gregorio Galante
@@ -186,6 +186,8 @@ files:
186
186
  - README.md
187
187
  - Rakefile
188
188
  - app/assets/config/lato_manifest.js
189
+ - app/assets/fonts/lato/bootstrap-icons.woff
190
+ - app/assets/fonts/lato/bootstrap-icons.woff2
189
191
  - app/assets/images/lato/user-150x150.jpg
190
192
  - app/assets/images/lato/user-300x300.jpg
191
193
  - app/assets/images/lato/user-600x600.jpg
@@ -198,6 +200,7 @@ files:
198
200
  - app/assets/javascripts/lato/controllers/lato_account_webauthn_controller.js
199
201
  - app/assets/javascripts/lato/controllers/lato_action_controller.js
200
202
  - app/assets/javascripts/lato/controllers/lato_aside_opener_controller.js
203
+ - app/assets/javascripts/lato/controllers/lato_auto_submit_controller.js
201
204
  - app/assets/javascripts/lato/controllers/lato_confirm_controller.js
202
205
  - app/assets/javascripts/lato/controllers/lato_copyclip_controller.js
203
206
  - app/assets/javascripts/lato/controllers/lato_feedback_controller.js
@@ -214,7 +217,11 @@ files:
214
217
  - app/assets/javascripts/lato/controllers/lato_network_controller.js
215
218
  - app/assets/javascripts/lato/controllers/lato_operation_controller.js
216
219
  - app/assets/javascripts/lato/controllers/lato_tooltip_controller.js
220
+ - app/assets/javascripts/lato/controllers/lato_web3_sign_controller.js
217
221
  - app/assets/javascripts/lato/controllers/lato_webauthn_auth_controller.js
222
+ - app/assets/javascripts/lato/debounce.js
223
+ - app/assets/javascripts/lato/vendor/ethers.esm.min.js
224
+ - app/assets/stylesheets/lato/_bootstrap-icons.scss
218
225
  - app/assets/stylesheets/lato/application.scss
219
226
  - app/controllers/concerns/lato/componentable.rb
220
227
  - app/controllers/concerns/lato/layoutable.rb