lato 3.15.1 → 3.17.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 -0
- data/app/assets/javascripts/lato/controllers/lato_account_webauthn_controller.js +93 -0
- data/app/assets/javascripts/lato/controllers/lato_input_list_controller.js +74 -0
- data/app/assets/javascripts/lato/controllers/lato_webauthn_auth_controller.js +110 -0
- data/app/controllers/lato/account_controller.rb +72 -16
- data/app/controllers/lato/authentication_controller.rb +95 -15
- data/app/helpers/lato/components_helper.rb +4 -0
- data/app/models/lato/user.rb +131 -1
- data/app/views/lato/account/_form-authenticator.html.erb +4 -3
- data/app/views/lato/account/_form-webauthn.html.erb +66 -0
- data/app/views/lato/account/index.html.erb +17 -6
- data/app/views/lato/authentication/_form-authentication-method.html.erb +36 -0
- data/app/views/lato/authentication/_form-webauthn.html.erb +27 -0
- data/app/views/lato/authentication/authentication_method.html.erb +10 -0
- data/app/views/lato/authentication/webauthn.html.erb +10 -0
- data/app/views/lato/components/_form_item_input_list.html.erb +66 -0
- data/app/views/layouts/lato/_action.html.erb +1 -1
- data/config/locales/en.yml +29 -3
- data/config/locales/fr.yml +29 -3
- data/config/locales/it.yml +29 -3
- data/config/locales/ro.yml +29 -3
- data/config/routes.rb +6 -1
- data/db/migrate/20251206170443_add_webauthn_id_to_user.rb +6 -0
- data/lib/lato/config.rb +11 -3
- data/lib/lato/engine.rb +11 -0
- data/lib/lato/version.rb +1 -1
- metadata +26 -2
data/app/models/lato/user.rb
CHANGED
|
@@ -14,6 +14,7 @@ module Lato
|
|
|
14
14
|
validates :accepted_privacy_policy_version, presence: true
|
|
15
15
|
validates :accepted_terms_and_conditions_version, presence: true
|
|
16
16
|
validates :web3_address, uniqueness: true, allow_blank: true
|
|
17
|
+
validates :webauthn_id, uniqueness: true, allow_blank: true
|
|
17
18
|
|
|
18
19
|
# Relations
|
|
19
20
|
##
|
|
@@ -58,6 +59,10 @@ module Lato
|
|
|
58
59
|
!authenticator_secret.blank?
|
|
59
60
|
end
|
|
60
61
|
|
|
62
|
+
def webauthn_enabled?
|
|
63
|
+
webauthn_id.present? && webauthn_public_key.present?
|
|
64
|
+
end
|
|
65
|
+
|
|
61
66
|
# Helpers
|
|
62
67
|
##
|
|
63
68
|
|
|
@@ -251,7 +256,9 @@ module Lato
|
|
|
251
256
|
c_password_update_code('')
|
|
252
257
|
|
|
253
258
|
update(params.permit(:password, :password_confirmation).merge(
|
|
254
|
-
authenticator_secret: nil # Reset authenticator secret when password is updated
|
|
259
|
+
authenticator_secret: nil, # Reset authenticator secret when password is updated
|
|
260
|
+
webauthn_id: nil, # Reset webauthn credential when password is updated
|
|
261
|
+
webauthn_public_key: nil # Reset webauthn credential when password is updated
|
|
255
262
|
))
|
|
256
263
|
end
|
|
257
264
|
|
|
@@ -320,6 +327,93 @@ module Lato
|
|
|
320
327
|
true
|
|
321
328
|
end
|
|
322
329
|
|
|
330
|
+
def webauthn_registration_options
|
|
331
|
+
WebAuthn::Credential.options_for_create(
|
|
332
|
+
user: {
|
|
333
|
+
id: Base64.strict_encode64(webauthn_user_handle),
|
|
334
|
+
name: email,
|
|
335
|
+
display_name: full_name
|
|
336
|
+
},
|
|
337
|
+
exclude: webauthn_exclude_credentials.map { |cred| Base64.strict_encode64(cred) }
|
|
338
|
+
)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def webauthn_authentication_options
|
|
342
|
+
WebAuthn::Credential.options_for_get(
|
|
343
|
+
allow: webauthn_allow_credentials.map { |cred| Base64.strict_encode64(cred) }
|
|
344
|
+
)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def register_webauthn_credential(credential_payload, encoded_challenge)
|
|
348
|
+
if credential_payload.blank?
|
|
349
|
+
errors.add(:base, :webauthn_payload_missing)
|
|
350
|
+
return false
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
if encoded_challenge.blank?
|
|
354
|
+
errors.add(:base, :webauthn_challenge_missing)
|
|
355
|
+
return false
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
parsed_payload = JSON.parse(credential_payload)
|
|
359
|
+
credential = WebAuthn::Credential.from_create(parsed_payload)
|
|
360
|
+
credential.verify(Base64.strict_decode64(encoded_challenge))
|
|
361
|
+
|
|
362
|
+
update(
|
|
363
|
+
webauthn_id: Base64.strict_encode64(credential.raw_id),
|
|
364
|
+
webauthn_public_key: credential.public_key
|
|
365
|
+
)
|
|
366
|
+
rescue JSON::ParserError, WebAuthn::Error => e
|
|
367
|
+
Rails.logger.error(e)
|
|
368
|
+
errors.add(:base, :webauthn_registration_failed)
|
|
369
|
+
false
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def webauthn_authentication(params, encoded_challenge)
|
|
373
|
+
return false unless webauthn_enabled?
|
|
374
|
+
|
|
375
|
+
if params[:webauthn_credential].blank?
|
|
376
|
+
errors.add(:base, :webauthn_payload_missing)
|
|
377
|
+
return false
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
if encoded_challenge.blank?
|
|
381
|
+
errors.add(:base, :webauthn_challenge_missing)
|
|
382
|
+
return false
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
parsed_payload = JSON.parse(params[:webauthn_credential])
|
|
386
|
+
|
|
387
|
+
# Converti i campi da base64url a base64 standard per il gem webauthn
|
|
388
|
+
parsed_payload['rawId'] = base64url_to_base64(parsed_payload['rawId'])
|
|
389
|
+
parsed_payload['response']['clientDataJSON'] = base64url_to_base64(parsed_payload['response']['clientDataJSON'])
|
|
390
|
+
parsed_payload['response']['authenticatorData'] = base64url_to_base64(parsed_payload['response']['authenticatorData'])
|
|
391
|
+
parsed_payload['response']['signature'] = base64url_to_base64(parsed_payload['response']['signature'])
|
|
392
|
+
parsed_payload['response']['userHandle'] = base64url_to_base64(parsed_payload['response']['userHandle']) if parsed_payload['response']['userHandle']
|
|
393
|
+
|
|
394
|
+
credential = WebAuthn::Credential.from_get(parsed_payload)
|
|
395
|
+
|
|
396
|
+
credential.verify(
|
|
397
|
+
encoded_challenge,
|
|
398
|
+
public_key: webauthn_public_key,
|
|
399
|
+
sign_count: 0
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
true
|
|
403
|
+
rescue JSON::ParserError, WebAuthn::Error => e
|
|
404
|
+
Rails.logger.error(e)
|
|
405
|
+
Rails.logger.error(e.backtrace.join("\n"))
|
|
406
|
+
errors.add(:base, :webauthn_authentication_failed)
|
|
407
|
+
false
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def remove_webauthn_credential
|
|
411
|
+
update(
|
|
412
|
+
webauthn_id: nil,
|
|
413
|
+
webauthn_public_key: nil
|
|
414
|
+
)
|
|
415
|
+
end
|
|
416
|
+
|
|
323
417
|
def generate_authenticator_secret
|
|
324
418
|
update(authenticator_secret: ROTP::Base32.random)
|
|
325
419
|
end
|
|
@@ -367,5 +461,41 @@ module Lato
|
|
|
367
461
|
Rails.cache.write(cache_key, value, expires_in: 30.minutes)
|
|
368
462
|
value
|
|
369
463
|
end
|
|
464
|
+
|
|
465
|
+
private
|
|
466
|
+
|
|
467
|
+
def webauthn_user_handle
|
|
468
|
+
Digest::SHA256.digest("lato-user-#{id}")
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def webauthn_exclude_credentials
|
|
472
|
+
return [] unless webauthn_id.present?
|
|
473
|
+
|
|
474
|
+
[Base64.strict_decode64(webauthn_id)]
|
|
475
|
+
rescue ArgumentError
|
|
476
|
+
[]
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def webauthn_allow_credentials
|
|
480
|
+
return [] unless webauthn_id.present?
|
|
481
|
+
|
|
482
|
+
[Base64.strict_decode64(webauthn_id)]
|
|
483
|
+
rescue ArgumentError
|
|
484
|
+
[]
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def base64url_to_base64(str)
|
|
488
|
+
return nil if str.nil?
|
|
489
|
+
# Converti base64url in base64 standard
|
|
490
|
+
base64 = str.tr('-_', '+/')
|
|
491
|
+
# Aggiungi padding se necessario
|
|
492
|
+
case base64.length % 4
|
|
493
|
+
when 2
|
|
494
|
+
base64 += '=='
|
|
495
|
+
when 3
|
|
496
|
+
base64 += '='
|
|
497
|
+
end
|
|
498
|
+
base64
|
|
499
|
+
end
|
|
370
500
|
end
|
|
371
501
|
end
|
|
@@ -15,14 +15,15 @@ user ||= Lato::User.new
|
|
|
15
15
|
<img class="shadow-sm rounded" src="<%= user.authenticator_qr_code_base64 %>" />
|
|
16
16
|
</div>
|
|
17
17
|
<div class="d-flex flex-column justify-content-between w-100">
|
|
18
|
-
<div class="alert alert-
|
|
18
|
+
<div class="alert alert-success">
|
|
19
|
+
<h4 class="alert-heading"><%= I18n.t('lato.account_authenticator_ready_title') %></h4>
|
|
19
20
|
<p class="mb-0">
|
|
20
21
|
<%= raw I18n.t('lato.account_authenticator_ready_qr') %>
|
|
21
22
|
</p>
|
|
22
23
|
</div>
|
|
23
24
|
|
|
24
25
|
<div class="d-flex justify-content-end">
|
|
25
|
-
<%= lato_form_submit form, I18n.t('lato.
|
|
26
|
+
<%= lato_form_submit form, I18n.t('lato.account_authenticator_disable'), class: %w[btn-danger] %>
|
|
26
27
|
</div>
|
|
27
28
|
</div>
|
|
28
29
|
</div>
|
|
@@ -33,7 +34,7 @@ user ||= Lato::User.new
|
|
|
33
34
|
<%= raw I18n.t('lato.account_authenticator_start_description') %>
|
|
34
35
|
</p>
|
|
35
36
|
<p class="mb-0">
|
|
36
|
-
<%= lato_form_submit form, I18n.t('lato.
|
|
37
|
+
<%= lato_form_submit form, I18n.t('lato.account_authenticator_generate_qr_code'), class: %w[btn-primary] %>
|
|
37
38
|
</p>
|
|
38
39
|
</div>
|
|
39
40
|
<% end %>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<%
|
|
2
|
+
|
|
3
|
+
user ||= Lato::User.new
|
|
4
|
+
registration_options = session[:webauthn_registration_options]
|
|
5
|
+
form_data = { turbo_frame: '_self', controller: 'lato-form' }
|
|
6
|
+
|
|
7
|
+
if registration_options.present?
|
|
8
|
+
form_data[:controller] += ' lato-account-webauthn'
|
|
9
|
+
form_data['lato-account-webauthn-options-value'] = registration_options.to_json
|
|
10
|
+
form_data['lato-account-webauthn-error-message-value'] = I18n.t('lato.account_webauthn_in_progress_error')
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
%>
|
|
14
|
+
|
|
15
|
+
<%= turbo_frame_tag 'account_form-webauthn' do %>
|
|
16
|
+
<%= form_with model: user, url: lato.account_update_webauthn_action_path, data: form_data do |form| %>
|
|
17
|
+
<%= lato_form_notices class: %w[mb-3] %>
|
|
18
|
+
<%= lato_form_errors user, class: %w[mb-3] %>
|
|
19
|
+
|
|
20
|
+
<% if user.webauthn_enabled? %>
|
|
21
|
+
<div class="alert alert-success mb-3">
|
|
22
|
+
<h4 class="alert-heading"><%= I18n.t('lato.account_webauthn_ready_title') %></h4>
|
|
23
|
+
<p class="mb-0">
|
|
24
|
+
<%= raw I18n.t('lato.account_webauthn_ready_description') %>
|
|
25
|
+
</p>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<%= form.hidden_field :webauthn_command, value: 'remove' %>
|
|
29
|
+
|
|
30
|
+
<div class="d-flex justify-content-end">
|
|
31
|
+
<%= lato_form_submit form, I18n.t('lato.account_webauthn_disable'), class: %w[btn-danger] %>
|
|
32
|
+
</div>
|
|
33
|
+
<% elsif registration_options.present? %>
|
|
34
|
+
<%= form.hidden_field :webauthn_command, value: 'complete' %>
|
|
35
|
+
<%= form.hidden_field :webauthn_credential, data: { lato_account_webauthn_target: 'credentialInput' } %>
|
|
36
|
+
|
|
37
|
+
<div class="alert alert-light mb-3" data-lato-account-webauthn-target="status">
|
|
38
|
+
<h4 class="alert-heading"><%= I18n.t('lato.account_webauthn_in_progress_title') %></h4>
|
|
39
|
+
<p class="mb-0">
|
|
40
|
+
<%= raw I18n.t('lato.account_webauthn_in_progress_description') %>
|
|
41
|
+
</p>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<%= lato_form_submit form, I18n.t('lato.account_webauthn_finalize'), class: %w[btn-primary d-none], data: { lato_account_webauthn_target: 'submit' } %>
|
|
45
|
+
|
|
46
|
+
<div class="d-flex justify-content-between align-items-center">
|
|
47
|
+
<%= link_to I18n.t('lato.account_webauthn_cancel'), lato.account_update_webauthn_action_path(user: { webauthn_command: 'cancel' }), class: 'btn btn-link px-0 text-decoration-none', data: { turbo_frame: '_self', turbo_method: :patch } %>
|
|
48
|
+
<div class="text-muted small d-flex align-items-center">
|
|
49
|
+
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
|
50
|
+
<span><%= I18n.t('lato.account_webauthn_waiting_browser') %></span>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<% else %>
|
|
54
|
+
<div class="alert alert-light mb-0">
|
|
55
|
+
<h4 class="alert-heading"><%= I18n.t('lato.account_webauthn_start_title') %></h4>
|
|
56
|
+
<p>
|
|
57
|
+
<%= raw I18n.t('lato.account_webauthn_start_description') %>
|
|
58
|
+
</p>
|
|
59
|
+
<p class="mb-0">
|
|
60
|
+
<%= form.hidden_field :webauthn_command, value: 'start' %>
|
|
61
|
+
<%= lato_form_submit form, I18n.t('lato.account_webauthn_enable'), class: %w[btn-primary] %>
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
<% end %>
|
|
65
|
+
<% end %>
|
|
66
|
+
<% end %>
|
|
@@ -21,24 +21,35 @@
|
|
|
21
21
|
</div>
|
|
22
22
|
</div>
|
|
23
23
|
|
|
24
|
-
<% if Lato.config.
|
|
24
|
+
<% if Lato.config.authenticator_connection %>
|
|
25
25
|
<div class="card mb-4">
|
|
26
26
|
<div class="card-header">
|
|
27
|
-
<h2 class="fs-4 mb-0"><%= I18n.t('lato.
|
|
27
|
+
<h2 class="fs-4 mb-0"><%= I18n.t('lato.account_authenticator') %></h2>
|
|
28
28
|
</div>
|
|
29
29
|
<div class="card-body">
|
|
30
|
-
<%= render 'lato/account/form-
|
|
30
|
+
<%= render 'lato/account/form-authenticator', user: @session.user %>
|
|
31
31
|
</div>
|
|
32
32
|
</div>
|
|
33
33
|
<% end %>
|
|
34
34
|
|
|
35
|
-
<% if Lato.config.
|
|
35
|
+
<% if Lato.config.webauthn_connection %>
|
|
36
36
|
<div class="card mb-4">
|
|
37
37
|
<div class="card-header">
|
|
38
|
-
<h2 class="fs-4 mb-0"><%= I18n.t('lato.
|
|
38
|
+
<h2 class="fs-4 mb-0"><%= I18n.t('lato.account_webauthn') %></h2>
|
|
39
39
|
</div>
|
|
40
40
|
<div class="card-body">
|
|
41
|
-
<%= render 'lato/account/form-
|
|
41
|
+
<%= render 'lato/account/form-webauthn', user: @session.user %>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<% end %>
|
|
45
|
+
|
|
46
|
+
<% if Lato.config.web3_connection %>
|
|
47
|
+
<div class="card mb-4">
|
|
48
|
+
<div class="card-header">
|
|
49
|
+
<h2 class="fs-4 mb-0"><%= I18n.t('lato.account_web3') %></h2>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="card-body">
|
|
52
|
+
<%= render 'lato/account/form-web3', user: @session.user %>
|
|
42
53
|
</div>
|
|
43
54
|
</div>
|
|
44
55
|
<% end %>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<%
|
|
2
|
+
|
|
3
|
+
user ||= Lato::User.new
|
|
4
|
+
|
|
5
|
+
%>
|
|
6
|
+
|
|
7
|
+
<%= turbo_frame_tag 'authentication_form-authentication-method' do %>
|
|
8
|
+
<%= form_with url: lato.authentication_authentication_method_action_path, method: :post, data: { turbo_frame: '_self' } do |form| %>
|
|
9
|
+
<%= lato_form_notices class: %w[mb-3] %>
|
|
10
|
+
<%= lato_form_errors user, class: %w[mb-3] %>
|
|
11
|
+
|
|
12
|
+
<% if user.authenticator_enabled? %>
|
|
13
|
+
<div class="mb-3">
|
|
14
|
+
<%= form.button I18n.t('lato.use_google_authenticator'),
|
|
15
|
+
type: 'submit',
|
|
16
|
+
name: 'method',
|
|
17
|
+
value: 'authenticator',
|
|
18
|
+
class: 'btn btn-primary w-100' %>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
21
|
+
|
|
22
|
+
<% if user.webauthn_enabled? %>
|
|
23
|
+
<div class="mb-3">
|
|
24
|
+
<%= form.button I18n.t('lato.use_webauthn'),
|
|
25
|
+
type: 'submit',
|
|
26
|
+
name: 'method',
|
|
27
|
+
value: 'webauthn',
|
|
28
|
+
class: 'btn btn-primary w-100' %>
|
|
29
|
+
</div>
|
|
30
|
+
<% end %>
|
|
31
|
+
|
|
32
|
+
<div class="text-center mt-3 mb-3">
|
|
33
|
+
<%= I18n.t('lato.or').downcase %> <%= link_to I18n.t('lato.reset_password').downcase, lato.authentication_recover_password_path %>
|
|
34
|
+
</div>
|
|
35
|
+
<% end %>
|
|
36
|
+
<% end %>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<%
|
|
2
|
+
|
|
3
|
+
user ||= Lato::User.new
|
|
4
|
+
options ||= {}
|
|
5
|
+
|
|
6
|
+
%>
|
|
7
|
+
|
|
8
|
+
<%= turbo_frame_tag 'authentication_form-webauthn' do %>
|
|
9
|
+
<%= form_with model: user, url: lato.authentication_webauthn_action_path, method: :post, data: { turbo_frame: '_self', controller: 'lato-form lato-webauthn-auth', 'lato-webauthn-auth-options-value': options.to_json } do |form| %>
|
|
10
|
+
<%= lato_form_notices class: %w[mb-3] %>
|
|
11
|
+
<%= lato_form_errors user, class: %w[mb-3] %>
|
|
12
|
+
|
|
13
|
+
<div class="mb-4 text-center">
|
|
14
|
+
<p><%= raw I18n.t('lato.webauthn_help', email: h(user.email_protected)) %></p>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<%= form.hidden_field :webauthn_credential, data: { 'lato-webauthn-auth-target': 'credential' } %>
|
|
18
|
+
|
|
19
|
+
<div class="mb-3">
|
|
20
|
+
<%= lato_form_submit form, I18n.t('lato.authenticate'), class: %w[d-block w-100], data: { action: 'lato-webauthn-auth#authenticate' } %>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="text-center mt-3 mb-3">
|
|
24
|
+
<%= I18n.t('lato.or').downcase %> <%= link_to I18n.t('lato.reset_password').downcase, lato.authentication_recover_password_path %>
|
|
25
|
+
</div>
|
|
26
|
+
<% end %>
|
|
27
|
+
<% end %>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<div class="w-100 h-100 d-flex justify-content-center align-items-center" style="min-height: calc(100vh - 54px - 2rem)">
|
|
2
|
+
<div class="card w-100" style="max-width: 400px">
|
|
3
|
+
<div class="card-header">
|
|
4
|
+
<h1 class="fs-3 mb-0 text-center"><%= I18n.t('lato.choose_authentication_method') %></h1>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="card-body">
|
|
7
|
+
<%= render 'lato/authentication/form-authentication-method', user: @user %>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<div class="w-100 h-100 d-flex justify-content-center align-items-center" style="min-height: calc(100vh - 54px - 2rem)">
|
|
2
|
+
<div class="card w-100" style="max-width: 400px">
|
|
3
|
+
<div class="card-header">
|
|
4
|
+
<h1 class="fs-3 mb-0 text-center"><%= I18n.t('lato.webauthn') %></h1>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="card-body">
|
|
7
|
+
<%= render 'lato/authentication/form-webauthn', user: @user, options: @options %>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<%
|
|
2
|
+
|
|
3
|
+
form ||= nil
|
|
4
|
+
key ||= nil
|
|
5
|
+
partial ||= nil
|
|
6
|
+
partial_params ||= {}
|
|
7
|
+
|
|
8
|
+
existing_records = form.object.send(key) || []
|
|
9
|
+
|
|
10
|
+
%>
|
|
11
|
+
|
|
12
|
+
<% unless partial %>
|
|
13
|
+
<div class="alert alert-warning">
|
|
14
|
+
Missing partial parameter for form_item_input_list partial.
|
|
15
|
+
</div>
|
|
16
|
+
<% else %>
|
|
17
|
+
<div data-controller="lato-input-list">
|
|
18
|
+
<div data-lato-input-list-target="list" id="<%= "#{form.object_name}_#{key}" %>">
|
|
19
|
+
<% existing_records.each_with_index do |record, index| %>
|
|
20
|
+
<%
|
|
21
|
+
item_nested_form = form.fields_for(key, record) do |f|
|
|
22
|
+
partial_params[:form] = f
|
|
23
|
+
end
|
|
24
|
+
%>
|
|
25
|
+
|
|
26
|
+
<div class="border p-2 mb-2 rounded" data-lato-input-list-target="listItem" data-index="<%= index %>">
|
|
27
|
+
<input type="hidden" name="<%= "#{form.object_name}[#{key}_attributes][#{index}][id]" %>" value="<%= record.id %>">
|
|
28
|
+
|
|
29
|
+
<%= render partial: partial, locals: {
|
|
30
|
+
form: item_nested_form,
|
|
31
|
+
**partial_params
|
|
32
|
+
} %>
|
|
33
|
+
|
|
34
|
+
<div class="d-flex justify-content-end mt-2">
|
|
35
|
+
<button type="button" class="btn btn-danger btn-sm" data-action="click->lato-input-list#removeItem" title="<%= I18n.t('lato.remove_item') %>" data-controller="lato-tooltip">
|
|
36
|
+
<i class="bi bi-trash"></i>
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="d-flex justify-content-end mt-2 pt-2 border-top">
|
|
44
|
+
<button type="button" class="btn btn-outline-primary btn-sm" data-action="click->lato-input-list#addItem">
|
|
45
|
+
<i class="bi bi-plus-circle"></i> <%= I18n.t('lato.add_item') %>
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<template data-lato-input-list-target="template">
|
|
50
|
+
<div class="template border p-2 mb-2 rounded">
|
|
51
|
+
<%= render partial: partial, locals: {
|
|
52
|
+
form: form.fields_for(key, form.object.send(key).new) do |f|
|
|
53
|
+
partial_params[:form] = f
|
|
54
|
+
end,
|
|
55
|
+
**partial_params
|
|
56
|
+
} %>
|
|
57
|
+
|
|
58
|
+
<div class="d-flex justify-content-end mt-2">
|
|
59
|
+
<button type="button" class="btn btn-danger btn-sm" data-action="click->lato-input-list#removeItem" title="<%= I18n.t('lato.remove_item') %>" data-controller="lato-tooltip">
|
|
60
|
+
<i class="bi bi-trash"></i>
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
</div>
|
|
66
|
+
<% end %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<% 10.times do %>
|
|
2
2
|
|
|
3
|
-
<div class="modal fade" data-lato-action-target="modal" tabindex="-1" aria-hidden="true" data-bs-keyboard="false">
|
|
3
|
+
<div class="modal fade" data-lato-action-target="modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
|
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">
|
data/config/locales/en.yml
CHANGED
|
@@ -53,12 +53,32 @@ en:
|
|
|
53
53
|
account_authenticator: Google Authenticator
|
|
54
54
|
account_authenticator_start_title: Enable Google Authenticator
|
|
55
55
|
account_authenticator_start_description: Generate a QR code by clicking the button below and scan it with the Google Authenticator app on your phone.<br>This will allow you to use the protect your account with a second factor authentication.
|
|
56
|
+
account_authenticator_ready_title: Google Authenticator ready
|
|
56
57
|
account_authenticator_ready_qr: Scan the QR code with the Google Authenticator app to use the account protection with a second factor authentication.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
authenticator:
|
|
58
|
+
account_authenticator_generate_qr_code: Generate QR code
|
|
59
|
+
account_authenticator_disable: Disconnect
|
|
60
|
+
authenticator: Google Authenticator
|
|
60
61
|
authenticator_code_help: Insert the code generated by the Google Authenticator app for the account <b>%{email}</b>
|
|
61
62
|
reset_password: Reset your password
|
|
63
|
+
choose_authentication_method: Choose authentication method
|
|
64
|
+
use_google_authenticator: Use Google Authenticator
|
|
65
|
+
use_webauthn: Use Passkey
|
|
66
|
+
authenticate: Authenticate
|
|
67
|
+
webauthn: Passkey Authentication
|
|
68
|
+
webauthn_help: Use your device to authenticate for account <b>%{email}</b>
|
|
69
|
+
account_webauthn: Passkey authentication
|
|
70
|
+
account_webauthn_start_title: Enable passkeys
|
|
71
|
+
account_webauthn_start_description: Use your device security (Touch ID, Face ID, Windows Hello, etc.) to approve future logins.<br>Click the button below and follow the browser prompts to register a passkey.
|
|
72
|
+
account_webauthn_enable: Register passkey
|
|
73
|
+
account_webauthn_in_progress_title: Complete the registration from your browser
|
|
74
|
+
account_webauthn_in_progress_description: Approve the prompt that appears in your browser to finish linking this passkey to your account.
|
|
75
|
+
account_webauthn_in_progress_error: Unable to register the passkey. Please restart the procedure and try again.
|
|
76
|
+
account_webauthn_waiting_browser: Waiting for your browser confirmation...
|
|
77
|
+
account_webauthn_cancel: Cancel request
|
|
78
|
+
account_webauthn_finalize: Complete registration
|
|
79
|
+
account_webauthn_ready_title: Passkey ready
|
|
80
|
+
account_webauthn_ready_description: This account now requires a WebAuthn challenge right after the classic login flow.
|
|
81
|
+
account_webauthn_disable: Disconnect
|
|
62
82
|
per_page: Per page
|
|
63
83
|
per_page_description: Number of elements per page
|
|
64
84
|
confirm_title: Confirm
|
|
@@ -71,6 +91,8 @@ en:
|
|
|
71
91
|
operation_failed_title: Operation failed
|
|
72
92
|
operation_failed_subtitle: The operation has failed because of an error
|
|
73
93
|
dropzone_drag_and_drop_or_click: Drag and drop files here or click to upload
|
|
94
|
+
add_item: Add item
|
|
95
|
+
remove_item: Remove item
|
|
74
96
|
|
|
75
97
|
invitation_mailer:
|
|
76
98
|
invite_mail_subject: You received an invitation
|
|
@@ -112,6 +134,10 @@ en:
|
|
|
112
134
|
web3_address_invalid: The address you send is not corretly signed
|
|
113
135
|
web3_connection_error: Impossible to connect the wallet
|
|
114
136
|
authenticator_code_invalid: The code you inserted is not correct
|
|
137
|
+
webauthn_payload_missing: Passkey response missing. Please try again.
|
|
138
|
+
webauthn_challenge_missing: Passkey challenge expired. Start the registration again.
|
|
139
|
+
webauthn_registration_failed: Unable to verify the passkey response. Please restart the procedure.
|
|
140
|
+
webauthn_authentication_failed: Passkey authentication failed. Please try again or use another method.
|
|
115
141
|
password:
|
|
116
142
|
not_correct: not correct
|
|
117
143
|
email:
|
data/config/locales/fr.yml
CHANGED
|
@@ -55,12 +55,32 @@ fr:
|
|
|
55
55
|
account_authenticator: Google Authenticator
|
|
56
56
|
account_authenticator_start_title: Activer Google Authenticator
|
|
57
57
|
account_authenticator_start_description: Générez un code QR en cliquant sur le bouton ci-dessous et scannez-le avec l'application Google Authenticator sur votre téléphone.<br>Cela vous permettra de protéger votre compte avec une authentification à deux facteurs.
|
|
58
|
+
account_authenticator_ready_title: Google Authenticator prêt
|
|
58
59
|
account_authenticator_ready_qr: Scannez le code QR avec l'application Google Authenticator pour activer la protection de votre compte avec une authentification à deux facteurs.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
authenticator:
|
|
60
|
+
account_authenticator_generate_qr_code: Générer un code QR
|
|
61
|
+
account_authenticator_disable: Déconnecter
|
|
62
|
+
authenticator: Google Authenticator
|
|
62
63
|
authenticator_code_help: Saisissez le code généré par l'application Google Authenticator pour le compte <b>%{email}</b>
|
|
63
64
|
reset_password: Réinitialisez votre mot de passe
|
|
65
|
+
choose_authentication_method: Choisir la méthode d'authentification
|
|
66
|
+
use_google_authenticator: Utiliser Google Authenticator
|
|
67
|
+
use_webauthn: Utiliser Passkey
|
|
68
|
+
authenticate: Authentifier
|
|
69
|
+
webauthn: Authentification Passkey
|
|
70
|
+
webauthn_help: Utilisez votre appareil pour vous authentifier sur le compte <b>%{email}</b>
|
|
71
|
+
account_webauthn: Authentification Passkey
|
|
72
|
+
account_webauthn_start_title: Activer les passkeys
|
|
73
|
+
account_webauthn_start_description: Utilisez la sécurité de votre appareil (Touch ID, Face ID, Windows Hello, etc.) pour approuver les connexions futures.<br>Cliquez sur le bouton ci-dessous et suivez les instructions de votre navigateur pour enregistrer une passkey.
|
|
74
|
+
account_webauthn_enable: Enregistrer la passkey
|
|
75
|
+
account_webauthn_in_progress_title: Validez l'inscription depuis votre navigateur
|
|
76
|
+
account_webauthn_in_progress_description: Approuvez la demande qui apparaît dans votre navigateur pour terminer l'association de la passkey à votre compte.
|
|
77
|
+
account_webauthn_in_progress_error: Impossible d'enregistrer la passkey. Redémarrez la procédure et réessayez.
|
|
78
|
+
account_webauthn_waiting_browser: En attente de la confirmation du navigateur...
|
|
79
|
+
account_webauthn_cancel: Annuler la demande
|
|
80
|
+
account_webauthn_finalize: Terminer l'inscription
|
|
81
|
+
account_webauthn_ready_title: Passkey activée
|
|
82
|
+
account_webauthn_ready_description: Ce compte nécessite désormais un défi WebAuthn juste après le processus de connexion classique.
|
|
83
|
+
account_webauthn_disable: Supprimer la passkey
|
|
64
84
|
per_page: Par page
|
|
65
85
|
per_page_description: Éléments par page
|
|
66
86
|
confirm_title: Confirmation
|
|
@@ -73,6 +93,8 @@ fr:
|
|
|
73
93
|
operation_failed_title: Échec de l'opération
|
|
74
94
|
operation_failed_subtitle: Une erreur s'est produite lors de l'opération
|
|
75
95
|
dropzone_drag_and_drop_or_click: Faites glisser et déposez les fichiers ici ou cliquez pour télécharger
|
|
96
|
+
add_item: Ajouter un élément
|
|
97
|
+
remove_item: Supprimer un élément
|
|
76
98
|
|
|
77
99
|
invitation_mailer:
|
|
78
100
|
invite_mail_subject: Vous avez reçu une invitation
|
|
@@ -120,6 +142,10 @@ fr:
|
|
|
120
142
|
web3_address_invalid: L'adresse envoyée n'est pas correctement signée
|
|
121
143
|
web3_connection_error: Impossible de connecter le portefeuille
|
|
122
144
|
authenticator_code_invalid: Le code saisi n'est pas correct
|
|
145
|
+
webauthn_payload_missing: Réponse passkey manquante. Veuillez réessayer.
|
|
146
|
+
webauthn_challenge_missing: Le défi passkey a expiré. Veuillez relancer la procédure.
|
|
147
|
+
webauthn_registration_failed: Impossible de vérifier la réponse passkey. Redémarrez la procédure.
|
|
148
|
+
webauthn_authentication_failed: Échec de l'authentification passkey. Veuillez réessayer ou utiliser une autre méthode.
|
|
123
149
|
password:
|
|
124
150
|
not_correct: incorrect
|
|
125
151
|
password_confirmation:
|
data/config/locales/it.yml
CHANGED
|
@@ -55,12 +55,32 @@ it:
|
|
|
55
55
|
account_authenticator: Google Authenticator
|
|
56
56
|
account_authenticator_start_title: Abilita Google Authenticator
|
|
57
57
|
account_authenticator_start_description: Genera un codice QR cliccando il pulsante sottostante e scansionalo con l'app Google Authenticator sul tuo telefono.<br>Questo ti permetterà di proteggere il tuo account con un'autenticazione a due fattori.
|
|
58
|
+
account_authenticator_ready_title: Google Authenticator pronto
|
|
58
59
|
account_authenticator_ready_qr: Scansiona il codice QR con l'app Google Authenticator per utilizzare la protezione dell'account con un'autenticazione a due fattori.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
authenticator:
|
|
60
|
+
account_authenticator_generate_qr_code: Genera codice QR
|
|
61
|
+
account_authenticator_disable: Disconnetti
|
|
62
|
+
authenticator: Google Authenticator
|
|
62
63
|
authenticator_code_help: Inserisci il codice generato dall'app Google Authenticator per l'account <b>%{email}</b>
|
|
63
64
|
reset_password: Reimposta la tua password
|
|
65
|
+
choose_authentication_method: Scegli il metodo di autenticazione
|
|
66
|
+
use_google_authenticator: Usa Google Authenticator
|
|
67
|
+
use_webauthn: Usa Passkey
|
|
68
|
+
authenticate: Autentica
|
|
69
|
+
webauthn: Autenticazione Passkey
|
|
70
|
+
webauthn_help: Usa il tuo dispositivo per autenticarti nell'account <b>%{email}</b>
|
|
71
|
+
account_webauthn: Autenticazione Passkey
|
|
72
|
+
account_webauthn_start_title: Abilita le Passkey
|
|
73
|
+
account_webauthn_start_description: Utilizza la sicurezza del tuo dispositivo (Touch ID, Face ID, Windows Hello, ecc.) per approvare gli accessi futuri.<br>Clicca il pulsante qui sotto e segui le istruzioni del browser per registrare una Passkey.
|
|
74
|
+
account_webauthn_enable: Registra Passkey
|
|
75
|
+
account_webauthn_in_progress_title: Completa la registrazione dal browser
|
|
76
|
+
account_webauthn_in_progress_description: Approva la richiesta che appare nel browser per terminare l'associazione della Passkey al tuo account.
|
|
77
|
+
account_webauthn_in_progress_error: Impossibile registrare la Passkey. Riavvia la procedura e riprova.
|
|
78
|
+
account_webauthn_waiting_browser: In attesa della conferma dal browser...
|
|
79
|
+
account_webauthn_cancel: Annulla richiesta
|
|
80
|
+
account_webauthn_finalize: Concludi registrazione
|
|
81
|
+
account_webauthn_ready_title: Passkey attiva
|
|
82
|
+
account_webauthn_ready_description: Questo account richiederà ora una sfida WebAuthn subito dopo il flusso di login classico.
|
|
83
|
+
account_webauthn_disable: Rimuovi Passkey
|
|
64
84
|
per_page: Per pagina
|
|
65
85
|
per_page_description: Elementi per pagina
|
|
66
86
|
confirm_title: Conferma
|
|
@@ -73,6 +93,8 @@ it:
|
|
|
73
93
|
operation_failed_title: Operazione fallita
|
|
74
94
|
operation_failed_subtitle: Si è verificato un errore durante l'operazione
|
|
75
95
|
dropzone_drag_and_drop_or_click: Trascina qui i file o clicca per caricare
|
|
96
|
+
add_item: Aggiungi elemento
|
|
97
|
+
remove_item: Rimuovi elemento
|
|
76
98
|
|
|
77
99
|
invitation_mailer:
|
|
78
100
|
invite_mail_subject: Hai ricevuto un invito
|
|
@@ -120,6 +142,10 @@ it:
|
|
|
120
142
|
web3_address_invalid: L'inidirizzo inviato non è correttamente firmato
|
|
121
143
|
web3_connection_error: Impossibile connettere il wallet
|
|
122
144
|
authenticator_code_invalid: Il codice inserito non è corretto
|
|
145
|
+
webauthn_payload_missing: Risposta Passkey mancante. Riprovare.
|
|
146
|
+
webauthn_challenge_missing: La sfida Passkey è scaduta. Avvia nuovamente la procedura.
|
|
147
|
+
webauthn_registration_failed: Impossibile verificare la risposta Passkey. Riavvia la procedura.
|
|
148
|
+
webauthn_authentication_failed: Autenticazione Passkey fallita. Riprova o usa un altro metodo.
|
|
123
149
|
password:
|
|
124
150
|
not_correct: non corretta
|
|
125
151
|
password_confirmation:
|