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.
- checksums.yaml +4 -4
- data/README.md +15 -32
- data/app/assets/config/lato_manifest.js +3 -1
- data/app/assets/fonts/lato/bootstrap-icons.woff +0 -0
- data/app/assets/fonts/lato/bootstrap-icons.woff2 +0 -0
- data/app/assets/javascripts/lato/controllers/lato_account_webauthn_controller.js +1 -1
- data/app/assets/javascripts/lato/controllers/lato_auto_submit_controller.js +8 -0
- data/app/assets/javascripts/lato/controllers/lato_input_autocomplete2_controller.js +2 -2
- data/app/assets/javascripts/lato/controllers/lato_input_autocomplete_controller.js +2 -2
- data/app/assets/javascripts/lato/controllers/lato_web3_sign_controller.js +30 -0
- data/app/assets/javascripts/lato/debounce.js +8 -0
- data/app/assets/javascripts/lato/vendor/ethers.esm.min.js +1 -0
- data/app/assets/stylesheets/lato/_bootstrap-icons.scss +2079 -0
- data/app/assets/stylesheets/lato/application.scss +18 -1
- data/app/controllers/concerns/lato/sessionable.rb +1 -0
- data/app/controllers/lato/authentication_controller.rb +1 -0
- data/app/helpers/lato/components_helper.rb +10 -1
- data/app/models/lato/invitation.rb +9 -0
- data/app/models/lato/session.rb +30 -1
- data/app/models/lato/user.rb +20 -4
- data/app/views/lato/account/_form-web3.html.erb +8 -37
- data/app/views/lato/account/_form-webauthn.html.erb +3 -3
- data/app/views/lato/account/_security-confirmation.html.erb +7 -4
- data/app/views/lato/account/index.html.erb +1 -1
- data/app/views/lato/authentication/_form-verify-email.html.erb +1 -2
- data/app/views/lato/authentication/_form-web3-signin.html.erb +6 -35
- data/app/views/lato/components/_form_item_input_signature.html.erb +1 -1
- data/app/views/lato/components/_index.html.erb +2 -2
- data/app/views/lato/components/_operation.html.erb +2 -2
- data/app/views/layouts/lato/application.html.erb +2 -2
- data/config/importmap.rb +4 -3
- data/config/locales/en.yml +2 -0
- data/config/locales/fr.yml +2 -0
- data/config/locales/it.yml +2 -0
- data/config/locales/ro.yml +2 -0
- data/lib/lato/config.rb +19 -0
- data/lib/lato/version.rb +1 -1
- 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
|
|
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
|
|
|
@@ -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
|
data/app/models/lato/session.rb
CHANGED
|
@@ -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)
|
|
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)
|
data/app/models/lato/user.rb
CHANGED
|
@@ -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
|
-
<
|
|
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
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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-
|
|
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"><%=
|
|
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><%=
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
8
|
-
pin "
|
|
9
|
-
|
|
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
|
data/config/locales/en.yml
CHANGED
|
@@ -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
|
data/config/locales/fr.yml
CHANGED
|
@@ -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
|
data/config/locales/it.yml
CHANGED
|
@@ -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
|
data/config/locales/ro.yml
CHANGED
|
@@ -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
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.
|
|
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
|