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