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
|
@@ -12,25 +12,25 @@ module Trek
|
|
|
12
12
|
source_root File.expand_path("templates", __dir__)
|
|
13
13
|
|
|
14
14
|
def install_dotfiles_and_tooling
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
15
|
+
generate_inline "trek:install:ruby"
|
|
16
|
+
generate_inline "trek:install:package_json"
|
|
17
|
+
generate_inline "trek:install:yarn"
|
|
18
|
+
|
|
19
|
+
generate_inline "trek:install:brakeman"
|
|
20
|
+
generate_inline "trek:install:bundler_audit"
|
|
21
|
+
generate_inline "trek:install:editorconfig"
|
|
22
|
+
generate_inline "trek:install:esbuild"
|
|
23
|
+
generate_inline "trek:install:eslint"
|
|
24
|
+
generate_inline "trek:install:lefthook"
|
|
25
|
+
generate_inline "trek:install:postcss"
|
|
26
|
+
generate_inline "trek:install:prettier"
|
|
27
|
+
generate_inline "trek:install:procfiles"
|
|
28
|
+
generate_inline "trek:install:prosopite"
|
|
29
|
+
generate_inline "trek:install:rubocop"
|
|
30
|
+
generate_inline "trek:install:slim_lint"
|
|
31
|
+
generate_inline "trek:install:stylelint"
|
|
32
|
+
generate_inline "trek:install:kaminari"
|
|
33
|
+
generate_inline "trek:install:lookbook"
|
|
34
34
|
|
|
35
35
|
run "yarn install", abort_on_failure: true
|
|
36
36
|
end
|
|
@@ -60,100 +60,101 @@ module Trek
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def setup_config
|
|
63
|
-
|
|
63
|
+
generate_inline "trek:install:config"
|
|
64
|
+
generate_inline "trek:install:security_headers"
|
|
64
65
|
end
|
|
65
66
|
|
|
66
67
|
def install_specs
|
|
67
|
-
|
|
68
|
+
generate_inline "trek:install:specs"
|
|
68
69
|
end
|
|
69
70
|
|
|
70
71
|
def install_strip_attributes
|
|
71
|
-
|
|
72
|
+
generate_inline "trek:install:strip_attributes"
|
|
72
73
|
end
|
|
73
74
|
|
|
74
75
|
def install_emails
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
generate_inline "trek:install:postmark"
|
|
77
|
+
generate_inline "trek:install:mailer"
|
|
77
78
|
end
|
|
78
79
|
|
|
79
80
|
def install_monitoring
|
|
80
|
-
|
|
81
|
+
generate_inline "trek:install:appsignal"
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
def install_user
|
|
84
|
-
|
|
85
|
+
generate_inline "trek:install:user"
|
|
85
86
|
end
|
|
86
87
|
|
|
87
88
|
def install_authentication
|
|
88
|
-
|
|
89
|
+
generate_inline "trek:install:authentication"
|
|
89
90
|
end
|
|
90
91
|
|
|
91
92
|
def install_authorization
|
|
92
|
-
|
|
93
|
+
generate_inline "trek:install:authorization"
|
|
93
94
|
end
|
|
94
95
|
|
|
95
96
|
def install_healthcheck
|
|
96
|
-
|
|
97
|
+
generate_inline "trek:install:healthcheck"
|
|
97
98
|
end
|
|
98
99
|
|
|
99
100
|
def install_translations
|
|
100
|
-
|
|
101
|
+
generate_inline "trek:install:translations"
|
|
101
102
|
end
|
|
102
103
|
|
|
103
104
|
def add_locales_generator
|
|
104
|
-
|
|
105
|
+
generate_inline "trek:install:locales"
|
|
105
106
|
end
|
|
106
107
|
|
|
107
108
|
def install_attachments
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
generate_inline "trek:install:attachments"
|
|
110
|
+
generate_inline "trek:install:uploaders"
|
|
110
111
|
end
|
|
111
112
|
|
|
112
113
|
def install_models
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
generate_inline "trek:install:page"
|
|
115
|
+
generate_inline "trek:install:current"
|
|
116
|
+
generate_inline "trek:install:fragment"
|
|
117
|
+
generate_inline "trek:install:menu_node"
|
|
118
|
+
generate_inline "trek:install:external_link"
|
|
119
|
+
generate_inline "trek:install:model_specs"
|
|
119
120
|
end
|
|
120
121
|
|
|
121
122
|
def install_ci
|
|
122
|
-
|
|
123
|
+
generate_inline "trek:install:ci"
|
|
123
124
|
end
|
|
124
125
|
|
|
125
126
|
def install_makefile
|
|
126
|
-
|
|
127
|
+
generate_inline "trek:install:makefile"
|
|
127
128
|
end
|
|
128
129
|
|
|
129
130
|
def install_readme
|
|
130
|
-
|
|
131
|
+
generate_inline "trek:install:readme"
|
|
131
132
|
end
|
|
132
133
|
|
|
133
134
|
def install_agents
|
|
134
|
-
|
|
135
|
+
generate_inline "trek:install:agents"
|
|
135
136
|
end
|
|
136
137
|
|
|
137
138
|
def install_admin
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
generate_inline "trek:install:admin"
|
|
140
|
+
generate_inline "trek:install:admin:routes"
|
|
141
|
+
generate_inline "trek:install:admin:fragments"
|
|
142
|
+
generate_inline "trek:install:admin:pages"
|
|
143
|
+
generate_inline "trek:install:admin:user_password_resets"
|
|
144
|
+
generate_inline "trek:install:admin:user_sessions"
|
|
145
|
+
generate_inline "trek:install:admin:users"
|
|
145
146
|
end
|
|
146
147
|
|
|
147
148
|
def install_assets
|
|
148
|
-
|
|
149
|
+
generate_inline "trek:install:assets"
|
|
149
150
|
end
|
|
150
151
|
|
|
151
152
|
def install_component_index
|
|
152
|
-
|
|
153
|
+
generate_inline "trek:install:component"
|
|
153
154
|
end
|
|
154
155
|
|
|
155
156
|
def install_application_files
|
|
156
|
-
|
|
157
|
+
generate_inline "trek:install:application"
|
|
157
158
|
end
|
|
158
159
|
|
|
159
160
|
def run_migrations
|
|
@@ -173,7 +174,7 @@ module Trek
|
|
|
173
174
|
say ""
|
|
174
175
|
say "Create an admin user with:"
|
|
175
176
|
say ""
|
|
176
|
-
say " rails g trek:admin:user --email admin@example.com --password
|
|
177
|
+
say " rails g trek:admin:user --email admin@example.com --password ************"
|
|
177
178
|
say ""
|
|
178
179
|
say "Then start the server:"
|
|
179
180
|
say ""
|
|
@@ -183,6 +184,13 @@ module Trek
|
|
|
183
184
|
|
|
184
185
|
private
|
|
185
186
|
|
|
187
|
+
# Runs a generator in this process rather than in a new `rails generate`, which boots the app
|
|
188
|
+
# again: for Trek's 50-odd generators, that took about two minutes. Other gems' generators keep
|
|
189
|
+
# their own process (`generate`), as they may need a gem this run just added to the Gemfile.
|
|
190
|
+
def generate_inline(name)
|
|
191
|
+
generate name, inline: true
|
|
192
|
+
end
|
|
193
|
+
|
|
186
194
|
def ascii_art
|
|
187
195
|
<<~ASCII
|
|
188
196
|
██████▌ ██████▌
|
|
@@ -44,6 +44,11 @@ module Trek
|
|
|
44
44
|
"app/policies/admin/tag_category_policy.rb"
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
def copy_model_specs
|
|
48
|
+
template "spec/models/tag_spec.rb",
|
|
49
|
+
"spec/models/tag_spec.rb"
|
|
50
|
+
end
|
|
51
|
+
|
|
47
52
|
def copy_policy_specs
|
|
48
53
|
template "spec/policies/admin/tag_policy_spec.rb",
|
|
49
54
|
"spec/policies/admin/tag_policy_spec.rb"
|
|
@@ -158,6 +158,22 @@ bundler-audit:
|
|
|
158
158
|
script:
|
|
159
159
|
- bundle exec bundle-audit check --update
|
|
160
160
|
|
|
161
|
+
# Software Bill of Materials (CycloneDX): the gems and npm packages in the lockfiles, with their licenses.
|
|
162
|
+
# Uncomment the job when a client asks for one.
|
|
163
|
+
# GitLab keeps each branch's latest one by default, past `expire_in`: hand over the one of the production branch.
|
|
164
|
+
# cdxgen runs with an empty environment (env -i), so neither it nor its dependencies see the CI's tokens.
|
|
165
|
+
# sbom:
|
|
166
|
+
# <<: *security_definition
|
|
167
|
+
# script:
|
|
168
|
+
# - env -i PATH="$PATH" HOME="$HOME" FETCH_LICENSE=true npx --yes @cyclonedx/cdxgen@12.8.6 --type ruby --type js --no-recurse --output sbom.cdx.json .
|
|
169
|
+
# artifacts:
|
|
170
|
+
# name: "sbom-${CI_COMMIT_REF_SLUG}"
|
|
171
|
+
# expire_in: 1 month
|
|
172
|
+
# paths:
|
|
173
|
+
# - sbom.cdx.json
|
|
174
|
+
# reports:
|
|
175
|
+
# cyclonedx: sbom.cdx.json
|
|
176
|
+
|
|
161
177
|
#
|
|
162
178
|
# Deploy
|
|
163
179
|
#
|
|
@@ -173,7 +189,7 @@ bundler-audit:
|
|
|
173
189
|
# - main
|
|
174
190
|
# environment:
|
|
175
191
|
# name: qa
|
|
176
|
-
# url: https://qa.
|
|
192
|
+
# url: https://qa.example.com
|
|
177
193
|
# script:
|
|
178
194
|
# - curl $HATCHBOX_WEBHOOK_QA -X POST
|
|
179
195
|
|
|
@@ -183,6 +199,6 @@ bundler-audit:
|
|
|
183
199
|
# - production
|
|
184
200
|
# environment:
|
|
185
201
|
# name: preprod
|
|
186
|
-
# url: https://
|
|
202
|
+
# url: https://example.com
|
|
187
203
|
# script:
|
|
188
204
|
# - curl $HATCHBOX_WEBHOOK_PROD -X POST
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Admin
|
|
2
|
+
# Signs out browsers signed in as the current user, listed on their profile's sessions tab. Only
|
|
3
|
+
# theirs: every query goes through Current.user.sessions.
|
|
4
|
+
class ActiveSessionsController < Admin::BaseController
|
|
5
|
+
def destroy
|
|
6
|
+
Current.user.sessions.find(params[:id]).destroy
|
|
7
|
+
redirect_to sessions_tab, notice: t(".success")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def destroy_others
|
|
11
|
+
Current.user.sessions.where.not(id: Current.session.id).destroy_all
|
|
12
|
+
redirect_to sessions_tab, notice: t(".success")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def sessions_tab
|
|
18
|
+
admin_user_path(Current.user, anchor: "sessions")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Admin
|
|
2
|
+
class AuditLogsController < Trek::ResourceController
|
|
3
|
+
include Trek::Filters
|
|
4
|
+
include Trek::Pagination
|
|
5
|
+
include Trek::Scopes
|
|
6
|
+
|
|
7
|
+
helper Trek::AuditLogsHelper
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
super
|
|
11
|
+
@objects = @objects.includes(:actor, :auditable)
|
|
12
|
+
apply_filters!
|
|
13
|
+
apply_scopes!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def model
|
|
19
|
+
AuditLog
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def available_filters
|
|
23
|
+
%i[action actor_type actor_id auditable_type auditable_id]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def available_scopes
|
|
27
|
+
%i[failures]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Admin
|
|
2
|
+
# Signing in with a passkey instead of a password, from the sign-in form
|
|
3
|
+
class PasskeySessionsController < ApplicationController
|
|
4
|
+
include Trek::Authentication
|
|
5
|
+
include Trek::Passkeys
|
|
6
|
+
|
|
7
|
+
allow_unauthenticated_access
|
|
8
|
+
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_admin_user_session_path, alert: t("notices.rate_limited") }
|
|
9
|
+
|
|
10
|
+
# What the browser needs to offer the site's passkeys, fetched when the sign-in form opens
|
|
11
|
+
def options
|
|
12
|
+
render_passkey_options User.passkey_request_options(passkey_relying_party)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create
|
|
16
|
+
@user = User.privileged.authenticate_by_passkey(passkey_relying_party, params[:credential], challenge: take_passkey_challenge)
|
|
17
|
+
|
|
18
|
+
if @user
|
|
19
|
+
# A passkey proves it's them: failed passwords no longer lock them out
|
|
20
|
+
@user.record_successful_sign_in!
|
|
21
|
+
start_new_session_for @user
|
|
22
|
+
record_sign_in_of @user
|
|
23
|
+
redirect_to after_authentication_url, notice: sign_in_notice(@user)
|
|
24
|
+
else
|
|
25
|
+
redirect_to new_admin_user_session_path, alert: t(".failure")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# In the user's language: this request isn't signed in yet, so it's still in the app's default one.
|
|
32
|
+
def sign_in_notice(user)
|
|
33
|
+
I18n.with_locale(user.locale || I18n.default_locale) { t("admin.user_sessions.create.success") }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# For the audit log (Trek::Auditable): the account signed in to
|
|
37
|
+
def audit_target
|
|
38
|
+
@user
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Admin
|
|
2
|
+
# Passkeys the current user adds and deletes, on their profile's passkeys tab. Only theirs: every
|
|
3
|
+
# query goes through Current.user.passkeys.
|
|
4
|
+
class PasskeysController < Admin::BaseController
|
|
5
|
+
include Trek::Passkeys
|
|
6
|
+
|
|
7
|
+
helper Trek::UserAgentsHelper
|
|
8
|
+
|
|
9
|
+
# What the browser needs to create a passkey, fetched when the user adds one
|
|
10
|
+
def options
|
|
11
|
+
render_passkey_options Current.user.passkey_creation_options(passkey_relying_party)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create
|
|
15
|
+
# Shown when Trek doesn't know the passkey's provider
|
|
16
|
+
browser = helpers.user_agent_summary(request.user_agent)
|
|
17
|
+
passkey = Current.user.add_passkey(passkey_relying_party, params[:credential], challenge: take_passkey_challenge, name: browser)
|
|
18
|
+
|
|
19
|
+
if passkey
|
|
20
|
+
redirect_to passkeys_tab, notice: t(".success")
|
|
21
|
+
else
|
|
22
|
+
redirect_to passkeys_tab, alert: t(".failure")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def destroy
|
|
27
|
+
Current.user.passkeys.find(params[:id]).destroy
|
|
28
|
+
redirect_to passkeys_tab, notice: t(".success"), status: :see_other
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def passkeys_tab
|
|
34
|
+
admin_user_path(Current.user, anchor: "passkeys")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -18,9 +18,9 @@ module Admin
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def create
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
# Queued whatever the email, and answered the same: the response doesn't tell whether an
|
|
22
|
+
# account exists.
|
|
23
|
+
Admin::PasswordResetJob.perform_later(params[:email].to_s)
|
|
24
24
|
|
|
25
25
|
redirect_to new_admin_user_session_path, notice: t(".success")
|
|
26
26
|
end
|
|
@@ -37,7 +37,7 @@ module Admin
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
@user.update!(params.permit(:password))
|
|
40
|
-
@user.
|
|
40
|
+
@user.sessions.destroy_all
|
|
41
41
|
redirect_to new_admin_user_session_path, notice: t(".success")
|
|
42
42
|
rescue ActiveRecord::RecordInvalid
|
|
43
43
|
redirect_to edit_admin_user_password_reset_path(@token), alert: @user.errors.full_messages.join(", ")
|
|
@@ -2,8 +2,13 @@ module Admin
|
|
|
2
2
|
class UserSessionsController < ApplicationController
|
|
3
3
|
include Trek::Authentication
|
|
4
4
|
|
|
5
|
+
# Failed sign-ins from one IP address, whatever the account, over a longer window than
|
|
6
|
+
# rate_limit's. Counted in Rails.cache, as rate_limit is.
|
|
7
|
+
FAILED_SIGN_INS_PER_IP = 20
|
|
8
|
+
FAILED_SIGN_INS_WINDOW = 1.hour
|
|
9
|
+
|
|
5
10
|
allow_unauthenticated_access only: %i[new create]
|
|
6
|
-
rate_limit to: 5, within: 3.minutes, only: :create, with: -> {
|
|
11
|
+
rate_limit to: 5, within: 3.minutes, only: :create, with: -> { render_rate_limited }
|
|
7
12
|
|
|
8
13
|
helper Trek::I18nHelper
|
|
9
14
|
helper Trek::ThemeHelper
|
|
@@ -19,11 +24,22 @@ module Admin
|
|
|
19
24
|
end
|
|
20
25
|
|
|
21
26
|
def create
|
|
22
|
-
|
|
27
|
+
return render_rate_limited if too_many_failed_sign_ins_from_ip?
|
|
28
|
+
|
|
29
|
+
# Hashes the password typed even when no user matches, so the response takes as long
|
|
30
|
+
user = User.privileged.authenticate_by(session_params)
|
|
31
|
+
@user = user || User.privileged.find_by(email: params[:email].to_s)
|
|
32
|
+
|
|
33
|
+
# A locked user is refused even with the right password, with the same answer as a wrong one
|
|
34
|
+
if user && !user.sign_in_locked?
|
|
35
|
+
user.record_successful_sign_in!
|
|
23
36
|
start_new_session_for user
|
|
24
|
-
|
|
37
|
+
record_sign_in_of user
|
|
38
|
+
redirect_to after_authentication_url, notice: sign_in_notice(user)
|
|
25
39
|
else
|
|
26
|
-
|
|
40
|
+
@user&.record_failed_sign_in!
|
|
41
|
+
count_failed_sign_in_from_ip
|
|
42
|
+
render_sign_in_form t(".failure"), status: :unprocessable_entity
|
|
27
43
|
end
|
|
28
44
|
end
|
|
29
45
|
|
|
@@ -34,8 +50,41 @@ module Admin
|
|
|
34
50
|
|
|
35
51
|
private
|
|
36
52
|
|
|
53
|
+
# In the user's language: this request isn't signed in yet, so it's still in the app's default one.
|
|
54
|
+
def sign_in_notice(user)
|
|
55
|
+
I18n.with_locale(user.locale || I18n.default_locale) { t(".success") }
|
|
56
|
+
end
|
|
57
|
+
|
|
37
58
|
def session_params
|
|
38
59
|
params.permit(:email, :password)
|
|
39
60
|
end
|
|
61
|
+
|
|
62
|
+
# Rendered rather than redirected to, so the audit log records the failure's status
|
|
63
|
+
def render_sign_in_form(alert, status:)
|
|
64
|
+
@email = params[:email].presence
|
|
65
|
+
flash.now[:alert] = alert
|
|
66
|
+
render :new, status:
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def render_rate_limited
|
|
70
|
+
render_sign_in_form t("notices.rate_limited"), status: :too_many_requests
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def too_many_failed_sign_ins_from_ip?
|
|
74
|
+
Rails.cache.read(failed_sign_ins_from_ip_key).to_i >= FAILED_SIGN_INS_PER_IP
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def count_failed_sign_in_from_ip
|
|
78
|
+
Rails.cache.increment(failed_sign_ins_from_ip_key, 1, expires_in: FAILED_SIGN_INS_WINDOW)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def failed_sign_ins_from_ip_key
|
|
82
|
+
"admin/failed_sign_ins/#{request.remote_ip}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# For the audit log (Trek::Auditable): the account signed in to, or tried
|
|
86
|
+
def audit_target
|
|
87
|
+
@user
|
|
88
|
+
end
|
|
40
89
|
end
|
|
41
90
|
end
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
module Admin
|
|
2
2
|
class UsersController < Trek::ResourceController
|
|
3
|
+
helper Trek::UserAgentsHelper
|
|
4
|
+
|
|
5
|
+
# On their own profile, users also see the browsers signed in as them (the sessions tab), and
|
|
6
|
+
# their passkeys.
|
|
7
|
+
def show
|
|
8
|
+
super
|
|
9
|
+
return unless @object == Current.user
|
|
10
|
+
|
|
11
|
+
@sessions = Current.user.sessions.active.recent_first
|
|
12
|
+
@passkeys = Current.user.passkeys.order(:created_at)
|
|
13
|
+
end
|
|
14
|
+
|
|
3
15
|
def new
|
|
4
16
|
super
|
|
5
17
|
@object.role = "editor"
|
|
@@ -12,6 +24,14 @@ module Admin
|
|
|
12
24
|
@object.password = SecureRandom.alphanumeric(24) if @object.password.blank?
|
|
13
25
|
end
|
|
14
26
|
|
|
27
|
+
# After a suspected compromise: every user, this admin included, must choose a new password at
|
|
28
|
+
# their next request.
|
|
29
|
+
def require_password_renewal
|
|
30
|
+
authorize! User, to: :require_password_renewal?
|
|
31
|
+
User.require_password_renewal!
|
|
32
|
+
redirect_to admin_users_path, notice: t(".success")
|
|
33
|
+
end
|
|
34
|
+
|
|
15
35
|
def theme
|
|
16
36
|
value = params[:theme].to_s
|
|
17
37
|
Current.user.update!(theme: value) if User.themes.key?(value)
|
|
@@ -22,15 +22,32 @@ module Trek::Authentication
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def require_authentication
|
|
25
|
-
|
|
25
|
+
return request_authentication unless resume_session
|
|
26
|
+
|
|
27
|
+
renew_password if Current.user.try(:password_renewal_needed?)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# An expired password, or one an admin asked everyone to renew, must be changed before going on:
|
|
31
|
+
# signs out and opens the new password form.
|
|
32
|
+
def renew_password
|
|
33
|
+
token = Current.user.password_reset_token
|
|
34
|
+
terminate_session
|
|
35
|
+
redirect_to edit_admin_user_password_reset_path(token), alert: t("notices.password_renewal_needed")
|
|
26
36
|
end
|
|
27
37
|
|
|
28
38
|
def resume_session
|
|
29
39
|
Current.session ||= find_session_by_cookie
|
|
30
40
|
end
|
|
31
41
|
|
|
42
|
+
# A session past its idle timeout or lifetime is dropped: the user signs in again.
|
|
32
43
|
def find_session_by_cookie
|
|
33
|
-
|
|
44
|
+
return unless cookies.signed[:session_id]
|
|
45
|
+
|
|
46
|
+
user_session = User::Session.find_by(id: cookies.signed[:session_id])
|
|
47
|
+
return user_session unless user_session&.expired?
|
|
48
|
+
|
|
49
|
+
user_session.destroy
|
|
50
|
+
nil
|
|
34
51
|
end
|
|
35
52
|
|
|
36
53
|
def request_authentication
|
|
@@ -43,20 +60,54 @@ module Trek::Authentication
|
|
|
43
60
|
end
|
|
44
61
|
|
|
45
62
|
def start_new_session_for(user)
|
|
46
|
-
user.
|
|
63
|
+
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
|
|
47
64
|
Current.session = session
|
|
48
|
-
|
|
65
|
+
# Its IP address's host name, for the active sessions list, without slowing the sign-in down
|
|
66
|
+
Trek::ResolveSessionHostnameJob.perform_later(session)
|
|
67
|
+
cookies.signed[:session_id] = {
|
|
68
|
+
value: session.id, expires: User::Session.lifetime.from_now, httponly: true, same_site: :lax
|
|
69
|
+
}
|
|
49
70
|
end
|
|
50
71
|
end
|
|
51
72
|
|
|
73
|
+
# Records the sign-in as the user's current one, and emails them when this browser never signed
|
|
74
|
+
# in as them before (unless it's their first sign-in, or the app turned it off).
|
|
75
|
+
def record_sign_in_of(user)
|
|
76
|
+
new_browser = !signed_in_from_this_browser?(user)
|
|
77
|
+
remember_sign_in_from_this_browser(user)
|
|
78
|
+
user.record_sign_in(ip: request.remote_ip, user_agent: request.user_agent)
|
|
79
|
+
return unless new_browser && user.last_sign_in_at && user.new_sign_in_email
|
|
80
|
+
|
|
81
|
+
AdminMailer.new_sign_in(
|
|
82
|
+
user, at: user.current_sign_in_at, ip: request.remote_ip, user_agent: request.user_agent
|
|
83
|
+
).deliver_later
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The users who signed in from this browser, in a signed cookie kept for years
|
|
87
|
+
def signed_in_from_this_browser?(user)
|
|
88
|
+
Array(cookies.signed[:trek_known_users]).include?(user.id)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def remember_sign_in_from_this_browser(user)
|
|
92
|
+
user_ids = (Array(cookies.signed[:trek_known_users]) | [user.id]).last(20)
|
|
93
|
+
cookies.signed.permanent[:trek_known_users] = { value: user_ids, httponly: true, same_site: :lax }
|
|
94
|
+
end
|
|
95
|
+
|
|
52
96
|
def terminate_session
|
|
53
97
|
Current.session.destroy
|
|
54
98
|
cookies.delete(:session_id)
|
|
55
99
|
end
|
|
56
100
|
|
|
101
|
+
# At most once every User::Session.activity_interval (5 minutes), so browsing doesn't write on
|
|
102
|
+
# every page.
|
|
57
103
|
def track_last_activity
|
|
58
|
-
#
|
|
59
|
-
Current.
|
|
60
|
-
|
|
104
|
+
# Not after signing out, which destroys the session
|
|
105
|
+
return unless Current.session&.persisted?
|
|
106
|
+
|
|
107
|
+
Current.session.record_activity
|
|
108
|
+
last_activity_at = Current.user.last_activity_at
|
|
109
|
+
return if last_activity_at && last_activity_at > User::Session.activity_interval.ago
|
|
110
|
+
|
|
111
|
+
Current.user.update_columns(last_activity_at: Time.current) # rubocop:disable Rails/SkipsModelValidations
|
|
61
112
|
end
|
|
62
113
|
end
|
|
@@ -3,10 +3,6 @@ module ApplicationHelper
|
|
|
3
3
|
# stylesheet_link_tag("https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap")
|
|
4
4
|
# end
|
|
5
5
|
|
|
6
|
-
def admin_webfonts_link_tag
|
|
7
|
-
stylesheet_link_tag("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap")
|
|
8
|
-
end
|
|
9
|
-
|
|
10
6
|
def admin_full_brand
|
|
11
7
|
[
|
|
12
8
|
t("admin.brand.title"),
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Records an AuditLog row for each mutation in the admin (see Trek::Auditable), including sign-ins
|
|
2
|
+
# and password resets. Include Trek::Auditable in another controller to audit it too.
|
|
3
|
+
Rails.application.config.to_prepare do
|
|
4
|
+
[
|
|
5
|
+
Trek::BaseController,
|
|
6
|
+
Admin::BaseController,
|
|
7
|
+
Admin::UserSessionsController,
|
|
8
|
+
Admin::PasskeySessionsController,
|
|
9
|
+
Admin::UserPasswordResetsController
|
|
10
|
+
].each { |controller| controller.include(Trek::Auditable) }
|
|
11
|
+
end
|