trek 2.3.1 → 2.4.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/.node-version +1 -1
- data/.yarn/releases/yarn-4.18.1.cjs +1000 -0
- data/.yarnrc.yml +3 -5
- data/AGENTS.md +7 -4
- data/CHANGELOG.md +55 -0
- data/Gemfile.lock +35 -1
- data/Makefile +12 -0
- data/app/assets/icons/trek/passkey.svg +5 -0
- data/app/assets/stylesheets/trek/base.css +6 -0
- data/app/components/trek/entries/item_component/item_component.css +6 -0
- data/app/components/trek/form/image_field_component/image_field_component.html.slim +9 -0
- data/app/components/trek/form/password_strength_component/password_strength_component.css +56 -0
- data/app/components/trek/form/password_strength_component/password_strength_component.html.slim +8 -0
- data/app/components/trek/form/password_strength_component/password_strength_component.js +74 -0
- data/app/components/trek/form/password_strength_component.rb +29 -0
- data/app/components/trek/gate_component/gate_component.css +7 -0
- data/app/components/trek/icon/sprite_component.rb +3 -2
- data/app/components/trek/section_component/section_component.css +22 -0
- data/app/components/trek/section_component/section_component.html.slim +5 -0
- data/app/components/trek/section_component/section_component.js +1 -0
- data/app/components/trek/section_component.rb +14 -0
- data/app/controllers/concerns/trek/auditable.rb +103 -0
- data/app/controllers/concerns/trek/filters.rb +2 -0
- data/app/controllers/concerns/trek/passkeys.rb +37 -0
- data/app/controllers/concerns/trek/scopes.rb +9 -2
- data/app/controllers/trek/csp_reports_controller.rb +62 -0
- data/app/controllers/trek/resource_controller.rb +7 -1
- data/app/helpers/trek/audit_logs_helper.rb +21 -0
- data/app/helpers/trek/user_agents_helper.rb +36 -0
- data/app/javascript/controllers/index.js +3 -0
- data/app/javascript/controllers/passkey_controller.js +118 -0
- data/app/javascript/trek.js +4 -0
- data/app/jobs/trek/audit_log/purge_job.rb +18 -0
- data/app/jobs/trek/purge_expired_sessions_job.rb +14 -0
- data/app/jobs/trek/resolve_session_hostname_job.rb +17 -0
- data/app/models/concerns/trek/audit_log.rb +30 -0
- data/app/models/concerns/trek/users/passkeys.rb +75 -0
- data/app/models/concerns/trek/users/password_history.rb +51 -0
- data/app/models/concerns/trek/users/password_policy.rb +85 -0
- data/app/models/concerns/trek/users/password_renewal.rb +51 -0
- data/app/models/concerns/trek/users/session_lifetime.rb +39 -0
- data/app/models/concerns/trek/users/sign_in_lockout.rb +67 -0
- data/app/models/concerns/trek/users/sign_ins.rb +31 -0
- data/app/models/concerns/trek/users/validations.rb +3 -2
- data/app/models/trek/feature.rb +6 -0
- data/app/models/trek/pwned_passwords.rb +40 -0
- data/app/models/trek/user_session.rb +10 -0
- data/app/policies/trek/audit_log_policy.rb +34 -0
- data/app/policies/trek/user_policy.rb +5 -0
- data/app/validators/not_disposable_validator.rb +18 -0
- data/config/brakeman.ignore +22 -0
- data/config/i18n-tasks.yml +7 -0
- data/config/locales/trek.en.yml +144 -39
- data/config/locales/trek.fr.yml +145 -40
- data/docs/.vitepress/config.mjs +8 -3
- data/docs/deploy.md +2 -0
- data/docs/proxies.md +53 -0
- data/docs/reference/components.md +1 -0
- data/docs/reference/controllers.md +1 -1
- data/docs/reference/environment-variables.md +2 -0
- data/docs/reference/generators/admin-user.md +1 -1
- data/docs/reference/generators/audit-log.md +113 -0
- data/docs/reference/generators/install.md +12 -3
- data/docs/reference/generators/security-headers.md +109 -0
- data/docs/reference/index.md +3 -0
- data/docs/reference/models/tag.md +4 -2
- data/docs/reference/models/user.md +123 -3
- data/docs/reference/validators.md +94 -0
- data/docs/security.md +90 -0
- data/lefthook.yml +8 -0
- data/lib/generators/trek/install/admin/user_password_resets_generator.rb +5 -0
- data/lib/generators/trek/install/admin/user_sessions_generator.rb +5 -0
- data/lib/generators/trek/install/admin/users_generator.rb +23 -0
- data/lib/generators/trek/install/audit_log_generator.rb +56 -0
- data/lib/generators/trek/install/authentication_generator.rb +77 -3
- data/lib/generators/trek/install/esbuild_generator.rb +1 -2
- data/lib/generators/trek/install/mailer_generator.rb +18 -0
- data/lib/generators/trek/install/model_specs_generator.rb +6 -4
- data/lib/generators/trek/install/postcss_generator.rb +1 -4
- data/lib/generators/trek/install/security_headers_generator.rb +49 -0
- data/lib/generators/trek/install/yarn_generator.rb +1 -1
- data/lib/generators/trek/install_generator.rb +62 -54
- data/lib/generators/trek/taxonomies_generator.rb +5 -0
- data/lib/generators/trek/templates/.gitlab-ci.yml +18 -2
- data/lib/generators/trek/templates/controllers/admin/active_sessions_controller.rb +21 -0
- data/lib/generators/trek/templates/controllers/admin/audit_logs_controller.rb +30 -0
- data/lib/generators/trek/templates/controllers/admin/passkey_sessions_controller.rb +41 -0
- data/lib/generators/trek/templates/controllers/admin/passkeys_controller.rb +37 -0
- data/lib/generators/trek/templates/controllers/admin/user_password_resets_controller.rb +4 -4
- data/lib/generators/trek/templates/controllers/admin/user_sessions_controller.rb +53 -4
- data/lib/generators/trek/templates/controllers/admin/users_controller.rb +20 -0
- data/lib/generators/trek/templates/controllers/concerns/trek/authentication.rb +58 -7
- data/lib/generators/trek/templates/helpers/application_helper.rb +0 -4
- data/lib/generators/trek/templates/initializers/audit_log.rb +11 -0
- data/lib/generators/trek/templates/initializers/content_security_policy.rb +49 -0
- data/lib/generators/trek/templates/initializers/permissions_policy.rb +19 -0
- data/lib/generators/trek/templates/jobs/admin/password_reset_job.rb +9 -0
- data/lib/generators/trek/templates/layouts/admin/auth.html.slim +0 -5
- data/lib/generators/trek/templates/layouts/admin.html.slim +0 -3
- data/lib/generators/trek/templates/mailers/admin/passwords_mailer.rb +1 -1
- data/lib/generators/trek/templates/mailers/admin_mailer.rb +28 -1
- data/lib/generators/trek/templates/migrations/audit_logs.rb.erb +27 -0
- data/lib/generators/trek/templates/migrations/tags.rb.erb +6 -1
- data/lib/generators/trek/templates/migrations/user_passkeys.rb.erb +24 -0
- data/lib/generators/trek/templates/migrations/user_password_renewal.rb.erb +8 -0
- data/lib/generators/trek/templates/migrations/user_previous_passwords.rb.erb +10 -0
- data/lib/generators/trek/templates/migrations/user_session_tracking.rb.erb +11 -0
- data/lib/generators/trek/templates/migrations/user_sign_in_lockout.rb.erb +7 -0
- data/lib/generators/trek/templates/migrations/user_sign_ins.rb.erb +13 -0
- data/lib/generators/trek/templates/models/audit_log.rb +3 -0
- data/lib/generators/trek/templates/models/tag_category.rb +2 -1
- data/lib/generators/trek/templates/models/user/passkey.rb +17 -0
- data/lib/generators/trek/templates/models/user/previous_password.rb +4 -0
- data/lib/generators/trek/templates/models/user/session.rb +8 -0
- data/lib/generators/trek/templates/policies/audit_log.rb +4 -0
- data/lib/generators/trek/templates/spec/mailers/admin/passwords_mailer_spec.rb +16 -0
- data/lib/generators/trek/templates/spec/mailers/admin_mailer_spec.rb +50 -0
- data/lib/generators/trek/templates/spec/models/audit_log_spec.rb +43 -0
- data/lib/generators/trek/templates/spec/models/tag_spec.rb +52 -0
- data/lib/generators/trek/templates/spec/models/user/session_spec.rb +68 -0
- data/lib/generators/trek/templates/spec/models/user_spec.rb +142 -4
- data/lib/generators/trek/templates/spec/requests/admin/active_sessions_spec.rb +119 -0
- data/lib/generators/trek/templates/spec/requests/admin/audit_logs_spec.rb +149 -0
- data/lib/generators/trek/templates/spec/requests/admin/passkey_sessions_spec.rb +103 -0
- data/lib/generators/trek/templates/spec/requests/admin/passkeys_spec.rb +104 -0
- data/lib/generators/trek/templates/spec/requests/admin/password_renewal_spec.rb +83 -0
- data/lib/generators/trek/templates/spec/requests/admin/sign_in_lockout_spec.rb +95 -0
- data/lib/generators/trek/templates/spec/requests/admin/sign_in_notices_spec.rb +93 -0
- data/lib/generators/trek/templates/spec/requests/admin/user_password_resets_spec.rb +55 -6
- data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +31 -2
- data/lib/generators/trek/templates/views/admin/audit_logs/index.html.slim +35 -0
- data/lib/generators/trek/templates/views/admin/audit_logs/show.html.slim +50 -0
- data/lib/generators/trek/templates/views/admin/user_password_resets/edit.html.slim +17 -7
- data/lib/generators/trek/templates/views/admin/user_password_resets/new.html.slim +3 -2
- data/lib/generators/trek/templates/views/admin/user_sessions/new.html.slim +28 -2
- data/lib/generators/trek/templates/views/admin/users/_passkeys.html.slim +67 -0
- data/lib/generators/trek/templates/views/admin/users/_properties.html.slim +21 -0
- data/lib/generators/trek/templates/views/admin/users/_sessions.html.slim +70 -0
- data/lib/generators/trek/templates/views/admin/users/index.html.slim +13 -0
- data/lib/generators/trek/templates/views/admin/users/show.html.slim +10 -21
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.en.html.slim +22 -0
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.en.text.erb +16 -0
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.fr.html.slim +22 -0
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.fr.text.erb +16 -0
- data/lib/generators/trek/templates/views/admin_mailer/sign_in_locked.en.html.slim +17 -0
- data/lib/generators/trek/templates/views/admin_mailer/sign_in_locked.en.text.erb +10 -0
- data/lib/generators/trek/templates/views/admin_mailer/sign_in_locked.fr.html.slim +18 -0
- data/lib/generators/trek/templates/views/admin_mailer/sign_in_locked.fr.text.erb +10 -0
- data/lib/trek/admin_ip_allowlist.rb +52 -0
- data/lib/trek/csp_reports.rb +99 -0
- data/lib/trek/data/common_passwords.txt +857 -0
- data/lib/trek/data/passkey_providers.json +404 -0
- data/lib/trek/disposable_email_domains.rb +53 -0
- data/lib/trek/engine.rb +12 -0
- data/lib/trek/passkey_providers.rb +19 -0
- data/lib/trek/reverse_dns.rb +27 -0
- data/lib/trek/version.rb +1 -1
- data/lib/trek.rb +13 -0
- data/package.json +3 -5
- data/renovate.json +11 -1
- data/trek.gemspec +3 -0
- data/yarn.lock +1757 -2019
- metadata +132 -4
- data/.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs +0 -541
- data/.yarn/releases/yarn-3.8.0.cjs +0 -875
- data/lib/generators/trek/templates/models/user_session.rb +0 -3
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
= render Trek::PropertiesComponent.new(model:) do |p|
|
|
2
|
+
- if @object.name?
|
|
3
|
+
= p.with_item :name
|
|
4
|
+
= @object.name
|
|
5
|
+
|
|
6
|
+
= p.with_item :role
|
|
7
|
+
= t("user.roles.#{@object.role}")
|
|
8
|
+
|
|
9
|
+
= p.with_item :email
|
|
10
|
+
= mail_to @object.email
|
|
11
|
+
|
|
12
|
+
= p.with_item :created_at
|
|
13
|
+
= l(@object.created_at, default: "")
|
|
14
|
+
|
|
15
|
+
- if policy_permitted_attribute?(:locale) && @object.locale?
|
|
16
|
+
= p.with_item :locale
|
|
17
|
+
= t("user.locale.#{@object.locale}")
|
|
18
|
+
|
|
19
|
+
- if policy_permitted_attribute?(:theme) && @object.theme?
|
|
20
|
+
= p.with_item :theme
|
|
21
|
+
= t("user.theme.#{@object.theme}")
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
- if @object.last_sign_in_at?
|
|
2
|
+
ruby:
|
|
3
|
+
change_password = link_to(
|
|
4
|
+
t(".change_password"),
|
|
5
|
+
new_admin_user_password_reset_path(email: @object.email),
|
|
6
|
+
)
|
|
7
|
+
not_you = t(".not_you_html", change_password:)
|
|
8
|
+
previous_sign_in = Trek::SectionComponent.new(
|
|
9
|
+
title: t(".previous_sign_in"),
|
|
10
|
+
hint: not_you,
|
|
11
|
+
)
|
|
12
|
+
= render previous_sign_in
|
|
13
|
+
= render Trek::EntriesComponent.new do |entries|
|
|
14
|
+
ruby:
|
|
15
|
+
entries.with_item(item: @object) do |entry|
|
|
16
|
+
browser = user_agent_summary(@object.last_sign_in_user_agent)
|
|
17
|
+
entry.with_title(href: nil) { browser }
|
|
18
|
+
entry.with_detail { @object.last_sign_in_ip }
|
|
19
|
+
entry.with_detail do
|
|
20
|
+
t(".signed_in", date: l(@object.last_sign_in_at, format: :long))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
= render Trek::SectionComponent.new(title: t(".title"), hint: t(".intro"))
|
|
25
|
+
= render Trek::EntriesComponent.new do |entries|
|
|
26
|
+
ruby:
|
|
27
|
+
sessions.each do |user_session|
|
|
28
|
+
entries.with_item(item: user_session) do |entry|
|
|
29
|
+
browser = user_agent_summary(user_session.user_agent)
|
|
30
|
+
entry.with_title(href: nil) { browser }
|
|
31
|
+
entry.with_detail do
|
|
32
|
+
ip = user_session.ip_address
|
|
33
|
+
hostname = user_session.ip_hostname
|
|
34
|
+
hostname ? tag.abbr(ip, title: hostname) : ip
|
|
35
|
+
end
|
|
36
|
+
entry.with_detail do
|
|
37
|
+
t(".signed_in", date: l(user_session.created_at, format: :long))
|
|
38
|
+
end
|
|
39
|
+
last_activity = time_ago_in_words(user_session.last_activity_at)
|
|
40
|
+
entry.with_detail { t(".last_activity", time: last_activity) }
|
|
41
|
+
entry.with_status do
|
|
42
|
+
if user_session == Current.session
|
|
43
|
+
t(".current")
|
|
44
|
+
else
|
|
45
|
+
render Trek::ButtonComponent.new(
|
|
46
|
+
href: admin_active_session_path(user_session),
|
|
47
|
+
text: t(".sign_out"),
|
|
48
|
+
size: :narrow,
|
|
49
|
+
data: { turbo_method: :delete },
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
- if sessions.size > 1
|
|
57
|
+
= render Trek::Form::ActionsComponent.new do |c|
|
|
58
|
+
ruby:
|
|
59
|
+
c.with_button(
|
|
60
|
+
href: %i[others admin active_sessions],
|
|
61
|
+
text: t(".sign_out_others"),
|
|
62
|
+
data: {
|
|
63
|
+
turbo_method: :delete,
|
|
64
|
+
turbo_confirm: t(".sign_out_others_confirm"),
|
|
65
|
+
turbo_confirm_button: t(".sign_out_others"),
|
|
66
|
+
turbo_confirm_color: "amber"
|
|
67
|
+
},
|
|
68
|
+
) do |b|
|
|
69
|
+
b.with_icon("trek/logout")
|
|
70
|
+
end
|
|
@@ -12,6 +12,19 @@
|
|
|
12
12
|
end
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
if allowed_to?(:require_password_renewal?, model)
|
|
16
|
+
c.with_button(
|
|
17
|
+
href: [:require_password_renewal, :admin, :users],
|
|
18
|
+
text: t(".require_password_renewal"),
|
|
19
|
+
data: {
|
|
20
|
+
turbo_method: :post,
|
|
21
|
+
turbo_confirm: t(".require_password_renewal_confirm"),
|
|
22
|
+
turbo_confirm_button: t(".require_password_renewal"),
|
|
23
|
+
turbo_confirm_color: "amber"
|
|
24
|
+
},
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
15
28
|
= render Trek::ListComponent.new(action: :show) do |c|
|
|
16
29
|
ruby:
|
|
17
30
|
@objects.each do |object|
|
|
@@ -18,24 +18,13 @@
|
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
=
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
= p.with_item :created_at
|
|
33
|
-
= l(@object.created_at, default: "")
|
|
34
|
-
|
|
35
|
-
- if policy_permitted_attribute?(:locale) && @object.locale?
|
|
36
|
-
= p.with_item :locale
|
|
37
|
-
= t("user.locale.#{@object.locale}")
|
|
38
|
-
|
|
39
|
-
- if policy_permitted_attribute?(:theme) && @object.theme?
|
|
40
|
-
= p.with_item :theme
|
|
41
|
-
= t("user.theme.#{@object.theme}")
|
|
21
|
+
- if @object == Current.user
|
|
22
|
+
= render Trek::TabsComponent.new do |c|
|
|
23
|
+
- c.with_item(id: :profile, label: t(".profile"))
|
|
24
|
+
= render "properties"
|
|
25
|
+
- c.with_item(id: :sessions, label: t(".active_sessions"))
|
|
26
|
+
= render "sessions", sessions: @sessions
|
|
27
|
+
- c.with_item(id: :passkeys, label: t(".passkeys"))
|
|
28
|
+
= render "passkeys", passkeys: @passkeys
|
|
29
|
+
- else
|
|
30
|
+
= render "properties"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
p Hello #{@user},
|
|
2
|
+
p
|
|
3
|
+
| Your #{t("application_name")} account was just signed in to
|
|
4
|
+
| from a browser it had never seen:
|
|
5
|
+
ul
|
|
6
|
+
li = l(@at, format: :long)
|
|
7
|
+
li = user_agent_summary(@user_agent)
|
|
8
|
+
li = @ip
|
|
9
|
+
p If it was you, there's nothing to do.
|
|
10
|
+
p
|
|
11
|
+
| If it wasn't, change your password right away,
|
|
12
|
+
| then sign out the browsers you don't recognize:
|
|
13
|
+
ul
|
|
14
|
+
li = link_to "Change your password",
|
|
15
|
+
@reset_url,
|
|
16
|
+
target: "_blank",
|
|
17
|
+
rel: "noopener"
|
|
18
|
+
li = link_to "See your active sessions",
|
|
19
|
+
@sessions_url,
|
|
20
|
+
target: "_blank",
|
|
21
|
+
rel: "noopener"
|
|
22
|
+
p The #{t("application_name")} team
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Hello <%%= @user %>,
|
|
2
|
+
|
|
3
|
+
Your <%%= t("application_name") %> account was just signed in to from a browser it had never seen:
|
|
4
|
+
|
|
5
|
+
- <%%= l(@at, format: :long) %>
|
|
6
|
+
- <%%= user_agent_summary(@user_agent) %>
|
|
7
|
+
- <%%= @ip %>
|
|
8
|
+
|
|
9
|
+
If it was you, there's nothing to do.
|
|
10
|
+
|
|
11
|
+
If it wasn't, change your password right away, then sign out the browsers you don't recognize:
|
|
12
|
+
|
|
13
|
+
- Change your password: <%%= @reset_url %>
|
|
14
|
+
- See your active sessions: <%%= @sessions_url %>
|
|
15
|
+
|
|
16
|
+
The <%%= t("application_name") %> team
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
p Bonjour #{@user},
|
|
2
|
+
p
|
|
3
|
+
| Votre compte #{t("application_name")} vient d'être connecté
|
|
4
|
+
| depuis un navigateur jamais vu :
|
|
5
|
+
ul
|
|
6
|
+
li = l(@at, format: :long)
|
|
7
|
+
li = user_agent_summary(@user_agent)
|
|
8
|
+
li = @ip
|
|
9
|
+
p Si c'était vous, il n'y a rien à faire.
|
|
10
|
+
p
|
|
11
|
+
| Sinon, changez votre mot de passe tout de suite,
|
|
12
|
+
| puis déconnectez les navigateurs que vous ne reconnaissez pas :
|
|
13
|
+
ul
|
|
14
|
+
li = link_to "Changer votre mot de passe",
|
|
15
|
+
@reset_url,
|
|
16
|
+
target: "_blank",
|
|
17
|
+
rel: "noopener"
|
|
18
|
+
li = link_to "Voir vos sessions actives",
|
|
19
|
+
@sessions_url,
|
|
20
|
+
target: "_blank",
|
|
21
|
+
rel: "noopener"
|
|
22
|
+
p L'équipe #{t("application_name")}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Bonjour <%%= @user %>,
|
|
2
|
+
|
|
3
|
+
Votre compte <%%= t("application_name") %> vient d'être connecté depuis un navigateur jamais vu :
|
|
4
|
+
|
|
5
|
+
- <%%= l(@at, format: :long) %>
|
|
6
|
+
- <%%= user_agent_summary(@user_agent) %>
|
|
7
|
+
- <%%= @ip %>
|
|
8
|
+
|
|
9
|
+
Si c'était vous, il n'y a rien à faire.
|
|
10
|
+
|
|
11
|
+
Sinon, changez votre mot de passe tout de suite, puis déconnectez les navigateurs que vous ne reconnaissez pas :
|
|
12
|
+
|
|
13
|
+
- Changer votre mot de passe : <%%= @reset_url %>
|
|
14
|
+
- Voir vos sessions actives : <%%= @sessions_url %>
|
|
15
|
+
|
|
16
|
+
L'équipe <%%= t("application_name") %>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
p Hello #{@user},
|
|
2
|
+
p
|
|
3
|
+
| Several attempts in a row to sign in to #{t("application_name")}
|
|
4
|
+
with your email failed, with a wrong password. To keep your account
|
|
5
|
+
safe, signing in is paused for a few minutes, a little longer after
|
|
6
|
+
each new failure.
|
|
7
|
+
p
|
|
8
|
+
| If it was you, wait a few minutes before trying again, or
|
|
9
|
+
=< link_to "choose a new password",
|
|
10
|
+
@reset_url,
|
|
11
|
+
target: "_blank",
|
|
12
|
+
rel: "noopener"
|
|
13
|
+
| .
|
|
14
|
+
p
|
|
15
|
+
| If it wasn't you, someone may be trying to guess your password: if
|
|
16
|
+
it's short or you use it elsewhere, choose a new one.
|
|
17
|
+
p The #{t("application_name")} team
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Hello <%%= @user %>,
|
|
2
|
+
|
|
3
|
+
Several attempts in a row to sign in to <%%= t("application_name") %> with your email failed, with a wrong password. To keep your account safe, signing in is paused for a few minutes, a little longer after each new failure.
|
|
4
|
+
|
|
5
|
+
If it was you, wait a few minutes before trying again, or choose a new password:
|
|
6
|
+
<%%= @reset_url %>
|
|
7
|
+
|
|
8
|
+
If it wasn't you, someone may be trying to guess your password: if it's short or you use it elsewhere, choose a new one.
|
|
9
|
+
|
|
10
|
+
The <%%= t("application_name") %> team
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
p Bonjour #{@user},
|
|
2
|
+
p
|
|
3
|
+
| Plusieurs tentatives de connexion à #{t("application_name")}
|
|
4
|
+
avec votre adresse e-mail ont échoué d'affilée, avec un mauvais mot de
|
|
5
|
+
passe. Pour protéger votre compte, la connexion est suspendue quelques
|
|
6
|
+
minutes, un peu plus longtemps après chaque nouvel échec.
|
|
7
|
+
p
|
|
8
|
+
| Si c'était vous, patientez quelques minutes avant de réessayer, ou
|
|
9
|
+
=< link_to "choisissez un nouveau mot de passe",
|
|
10
|
+
@reset_url,
|
|
11
|
+
target: "_blank",
|
|
12
|
+
rel: "noopener"
|
|
13
|
+
| .
|
|
14
|
+
p
|
|
15
|
+
| Si ce n'était pas vous, quelqu'un essaie peut-être de deviner votre mot
|
|
16
|
+
de passe : s'il est court ou que vous l'utilisez ailleurs,
|
|
17
|
+
choisissez-en un nouveau.
|
|
18
|
+
p L'équipe #{t("application_name")}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Bonjour <%%= @user %>,
|
|
2
|
+
|
|
3
|
+
Plusieurs tentatives de connexion à <%%= t("application_name") %> avec votre adresse e-mail ont échoué d'affilée, avec un mauvais mot de passe. Pour protéger votre compte, la connexion est suspendue quelques minutes, un peu plus longtemps après chaque nouvel échec.
|
|
4
|
+
|
|
5
|
+
Si c'était vous, patientez quelques minutes avant de réessayer, ou choisissez un nouveau mot de passe :
|
|
6
|
+
<%%= @reset_url %>
|
|
7
|
+
|
|
8
|
+
Si ce n'était pas vous, quelqu'un essaie peut-être de deviner votre mot de passe : s'il est court ou que vous l'utilisez ailleurs, choisissez-en un nouveau.
|
|
9
|
+
|
|
10
|
+
L'équipe <%%= t("application_name") %>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
module Trek
|
|
6
|
+
# Restricts the admin (/admin and below, the sign-in page included) to the IP addresses and ranges in
|
|
7
|
+
# ADMIN_ALLOWED_IPS, separated by commas or spaces: "203.0.113.0/24, 2001:db8::/32, 198.51.100.7".
|
|
8
|
+
# From anywhere else, the admin doesn't exist: a 404, as for an unknown URL, rather than a redirect
|
|
9
|
+
# to the sign-in page. Off when ADMIN_ALLOWED_IPS is unset or empty; read when the app starts.
|
|
10
|
+
#
|
|
11
|
+
# The address is `request.remote_ip`: behind a proxy or a CDN, list its ranges in
|
|
12
|
+
# `config.action_dispatch.trusted_proxies`, or every request seems to come from it (docs: proxies).
|
|
13
|
+
#
|
|
14
|
+
# Inserted after ActionDispatch::DebugExceptions (Trek::Engine): after RemoteIp, which works out the
|
|
15
|
+
# address, and inside ShowExceptions, which turns the routing error into the app's 404 page.
|
|
16
|
+
class AdminIpAllowlist
|
|
17
|
+
PATH = %r{\A/admin(/|\z)}
|
|
18
|
+
|
|
19
|
+
def initialize(app, allowed = ENV.fetch("ADMIN_ALLOWED_IPS", nil))
|
|
20
|
+
@app = app
|
|
21
|
+
@ranges = parse(allowed)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(env)
|
|
25
|
+
if @ranges.any? && env["PATH_INFO"].to_s.match?(PATH) && !allowed?(ActionDispatch::Request.new(env).remote_ip)
|
|
26
|
+
# The same error, and log line, as for a URL no route matches
|
|
27
|
+
raise ActionController::RoutingError, "No route matches [#{env["REQUEST_METHOD"]}] #{env["PATH_INFO"].inspect}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
@app.call(env)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def parse(allowed)
|
|
36
|
+
allowed.to_s.split(/[\s,]+/).reject(&:empty?).map do |range|
|
|
37
|
+
IPAddr.new(range)
|
|
38
|
+
rescue IPAddr::Error
|
|
39
|
+
raise ArgumentError, "ADMIN_ALLOWED_IPS: #{range.inspect} is neither an IP address nor a range"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def allowed?(remote_ip)
|
|
44
|
+
ip = IPAddr.new(remote_ip.to_s)
|
|
45
|
+
# An IPv4 address may come as IPv6 ("::ffff:203.0.113.5")
|
|
46
|
+
ip = ip.native if ip.ipv4_mapped?
|
|
47
|
+
@ranges.any? { |range| range.family == ip.family && range.include?(ip) }
|
|
48
|
+
rescue IPAddr::Error
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Trek
|
|
7
|
+
# A Content Security Policy violation, as an error for AppSignal (Trek::CspReportsController)
|
|
8
|
+
class CspViolation < StandardError; end
|
|
9
|
+
|
|
10
|
+
# The violations in a Content Security Policy report, as browsers send them to the policy's
|
|
11
|
+
# report-uri: `application/csp-report` (`{"csp-report": {…}}`), or the Reporting API's
|
|
12
|
+
# `application/reports+json` (`[{"type": "csp-violation", "body": {…}}]`).
|
|
13
|
+
#
|
|
14
|
+
# Kept: the app's own pages only (a report can claim any page), and not what browser extensions
|
|
15
|
+
# inject or load, which makes most reports and says nothing about the app. As anyone can forge a
|
|
16
|
+
# report, it must be plausible too: a known directive, and a keyword or a host name blocked.
|
|
17
|
+
#
|
|
18
|
+
# A violation keeps the page's path, without its query, which may carry a token, and the blocked
|
|
19
|
+
# resource's origin.
|
|
20
|
+
module CspReports
|
|
21
|
+
EXTENSION_SCHEMES = %w[chrome-extension moz-extension safari-extension safari-web-extension ms-browser-extension].freeze
|
|
22
|
+
|
|
23
|
+
# The directives a browser reports a violation of
|
|
24
|
+
DIRECTIVES = %w[
|
|
25
|
+
base-uri child-src connect-src default-src font-src form-action frame-ancestors frame-src img-src
|
|
26
|
+
manifest-src media-src object-src require-trusted-types-for script-src script-src-attr
|
|
27
|
+
script-src-elem style-src style-src-attr style-src-elem trusted-types worker-src
|
|
28
|
+
].freeze
|
|
29
|
+
# What a browser reports as blocked instead of a URL
|
|
30
|
+
KEYWORDS = %w[inline eval wasm-eval data blob trusted-types-policy trusted-types-sink].freeze
|
|
31
|
+
# Schemes of a blocked URL, which keeps its origin
|
|
32
|
+
SCHEMES = %w[http https ws wss].freeze
|
|
33
|
+
HOST_NAME = /\A(?=.{1,253}\z)[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\z/i
|
|
34
|
+
|
|
35
|
+
Violation = Struct.new(:directive, :blocked, :page, :disposition, keyword_init: true) do
|
|
36
|
+
def to_s
|
|
37
|
+
"#{directive} blocked #{blocked} on #{page} (#{disposition})"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The violations about pages of `host`
|
|
42
|
+
def self.parse(body, host:)
|
|
43
|
+
reports = JSON.parse(body)
|
|
44
|
+
fields =
|
|
45
|
+
if reports.is_a?(Array)
|
|
46
|
+
reports.select { |report| report.is_a?(Hash) && report["type"] == "csp-violation" }.map { |report| report["body"] }
|
|
47
|
+
elsif reports.is_a?(Hash)
|
|
48
|
+
[reports["csp-report"]]
|
|
49
|
+
end
|
|
50
|
+
Array(fields).select { |field| field.is_a?(Hash) }.filter_map { |field| violation(field, host:) }
|
|
51
|
+
rescue JSON::ParserError
|
|
52
|
+
[]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.violation(fields, host:)
|
|
56
|
+
page = parse_uri(fields["document-uri"] || fields["documentURL"])
|
|
57
|
+
return unless page&.host == host
|
|
58
|
+
|
|
59
|
+
blocked = fields["blocked-uri"] || fields["blockedURL"]
|
|
60
|
+
source = fields["source-file"] || fields["sourceFile"]
|
|
61
|
+
return if [blocked, source].any? { |uri| extension?(uri) }
|
|
62
|
+
|
|
63
|
+
directive = fields["effective-directive"] || fields["effectiveDirective"] || fields["violated-directive"].to_s.split.first
|
|
64
|
+
blocked = origin(blocked)
|
|
65
|
+
return unless DIRECTIVES.include?(directive) && blocked
|
|
66
|
+
|
|
67
|
+
Violation.new(
|
|
68
|
+
directive:,
|
|
69
|
+
blocked:,
|
|
70
|
+
page: page.path.presence || "/",
|
|
71
|
+
disposition: fields["disposition"] || "enforce"
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
private_class_method :violation
|
|
75
|
+
|
|
76
|
+
# A URL's origin, or the keyword a browser reports instead ("inline", "eval", "data"…); nil when
|
|
77
|
+
# it's neither
|
|
78
|
+
def self.origin(value)
|
|
79
|
+
uri = parse_uri(value)
|
|
80
|
+
return KEYWORDS.find { |keyword| keyword == (value.to_s.presence || "inline") } unless uri&.host
|
|
81
|
+
return unless SCHEMES.include?(uri.scheme) && uri.host.match?(HOST_NAME)
|
|
82
|
+
|
|
83
|
+
"#{uri.scheme}://#{uri.host}#{":#{uri.port}" unless uri.port == uri.default_port}"
|
|
84
|
+
end
|
|
85
|
+
private_class_method :origin
|
|
86
|
+
|
|
87
|
+
def self.extension?(value)
|
|
88
|
+
EXTENSION_SCHEMES.include?(value.to_s.split(":", 2).first)
|
|
89
|
+
end
|
|
90
|
+
private_class_method :extension?
|
|
91
|
+
|
|
92
|
+
def self.parse_uri(value)
|
|
93
|
+
URI.parse(value.to_s)
|
|
94
|
+
rescue URI::InvalidURIError
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
private_class_method :parse_uri
|
|
98
|
+
end
|
|
99
|
+
end
|