trek 2.5.0 → 2.6.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/AGENTS.md +7 -4
- data/CHANGELOG.md +37 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +16 -3
- data/Makefile +2 -0
- data/app/components/trek/dashboard/tile_component/tile_component.css +9 -0
- data/app/components/trek/dashboard/tile_component/tile_component.html.slim +4 -2
- data/app/components/trek/dashboard/tile_component.rb +12 -2
- data/app/components/trek/form/content_editor_component/content_editor_component.css +5 -0
- data/app/components/trek/form/content_editor_component/content_editor_component.html.slim +19 -93
- data/app/components/trek/form/content_editor_component/content_editor_component.js +94 -251
- data/app/components/trek/form/content_editor_component/extensions/block_view.js +40 -0
- data/app/components/trek/form/content_editor_component/extensions/image_block.js +28 -26
- data/app/components/trek/form/content_editor_component/extensions/prompts_block.js +15 -26
- data/app/components/trek/form/content_editor_component/extensions/table_of_contents.js +1 -3
- data/app/components/trek/form/content_editor_component.rb +15 -0
- data/app/components/trek/form/image_field_component/image_field_component.css +40 -1
- data/app/components/trek/form/image_field_component/image_field_component.html.slim +0 -9
- data/app/components/trek/form/image_field_component.rb +36 -9
- data/app/content/trek/content/cleaner.rb +53 -0
- data/app/content/trek/content/document.rb +2 -2
- data/app/content/trek/content/prompts_block.rb +1 -2
- data/app/content/trek/content.rb +6 -0
- data/app/controllers/concerns/trek/reorderable.rb +24 -6
- data/app/controllers/trek/csp_reports_controller.rb +14 -1
- data/app/controllers/trek/panels/prompts_controller.rb +1 -1
- data/app/controllers/trek/resource_controller.rb +5 -5
- data/app/jobs/trek/purge_expired_sessions_job.rb +4 -3
- data/app/models/concerns/trek/contentable.rb +15 -1
- data/app/models/concerns/trek/users/sign_in_code.rb +70 -0
- data/app/models/concerns/trek/users/sign_in_lockout.rb +4 -2
- data/app/views/trek/panels/links/_form.html.slim +2 -4
- data/app/views/trek/panels/prompts/create.turbo_stream.slim +0 -1
- data/config/locales/trek.en.yml +26 -11
- data/config/locales/trek.fr.yml +26 -11
- data/docs/.vitepress/config.mjs +4 -1
- data/docs/background-jobs.md +69 -0
- data/docs/content-editor.md +219 -0
- data/docs/customize.md +2 -0
- data/docs/deploy.md +2 -2
- data/docs/getting-started.md +1 -1
- data/docs/index.md +1 -1
- data/docs/reference/components.md +2 -2
- data/docs/reference/concerns/contentable.md +10 -4
- data/docs/reference/concerns/formattable.md +1 -1
- data/docs/reference/environment-variables.md +1 -0
- data/docs/reference/formatters.md +2 -30
- data/docs/reference/forms.md +1 -1
- data/docs/reference/generators/audit-log.md +2 -2
- data/docs/reference/generators/install.md +3 -2
- data/docs/reference/generators/public-accounts.md +141 -0
- data/docs/reference/generators/security-headers.md +1 -1
- data/docs/reference/index.md +2 -1
- data/docs/reference/models/user.md +3 -1
- data/docs/what-is-trek.md +1 -1
- data/lib/generators/trek/audit_log_generator.rb +54 -0
- data/lib/generators/trek/install/admin/users_generator.rb +6 -1
- data/lib/generators/trek/install/audit_log_generator.rb +9 -45
- data/lib/generators/trek/install/good_job_generator.rb +161 -0
- data/lib/generators/trek/install/model_specs_generator.rb +3 -3
- data/lib/generators/trek/install/ruby_generator.rb +1 -1
- data/lib/generators/trek/install/scalingo_generator.rb +7 -45
- data/lib/generators/trek/install_generator.rb +15 -0
- data/lib/generators/trek/public_accounts_generator.rb +267 -0
- data/lib/generators/trek/scalingo_generator.rb +55 -0
- data/lib/generators/trek/templates/.gitlab-ci.yml +1 -1
- data/lib/generators/trek/templates/controllers/admin/password_renewals_controller.rb +11 -0
- data/lib/generators/trek/templates/controllers/admin/user_sessions_controller.rb +1 -1
- data/lib/generators/trek/templates/controllers/admin/users_controller.rb +0 -8
- data/lib/generators/trek/templates/initializers/content_security_policy.rb +3 -2
- data/lib/generators/trek/templates/initializers/good_job.rb.tt +38 -0
- data/lib/generators/trek/templates/jobs/mail_delivery_job.rb +6 -0
- data/lib/generators/trek/templates/layouts/admin.html.slim +5 -0
- data/lib/generators/trek/templates/mailers/admin_mailer.rb +4 -2
- data/lib/generators/trek/templates/package.json.tt +1 -1
- data/lib/generators/trek/templates/public_accounts/api/authentication.rb.tt +76 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/accounts_controller.rb.tt +22 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/active_sessions_controller.rb.tt +21 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/base_controller.rb.tt +71 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/email_changes_controller.rb.tt +55 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/email_confirmations_controller.rb.tt +19 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/password_resets_controller.rb.tt +28 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/passwords_controller.rb.tt +17 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/registrations_controller.rb.tt +17 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/sessions_controller.rb.tt +45 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/sign_in_codes_controller.rb.tt +15 -0
- data/lib/generators/trek/templates/public_accounts/jobs/sign_in_code_job.rb.tt +13 -0
- data/lib/generators/trek/templates/public_accounts/locales/accounts.en.yml.tt +205 -0
- data/lib/generators/trek/templates/public_accounts/locales/accounts.fr.yml.tt +205 -0
- data/lib/generators/trek/templates/public_accounts/mailers/account_mailer.rb.tt +85 -0
- data/lib/generators/trek/templates/public_accounts/migrations/create_accounts.rb.tt +46 -0
- data/lib/generators/trek/templates/public_accounts/models/account/session.rb.tt +47 -0
- data/lib/generators/trek/templates/public_accounts/models/account/sign_in_code.rb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/models/account.rb.tt +73 -0
- data/lib/generators/trek/templates/public_accounts/spec/api/accounts_spec.rb.tt +90 -0
- data/lib/generators/trek/templates/public_accounts/spec/api/sign_ins_spec.rb.tt +158 -0
- data/lib/generators/trek/templates/public_accounts/spec/models/account/sign_in_code_spec.rb.tt +56 -0
- data/lib/generators/trek/templates/public_accounts/spec/models/account_spec.rb.tt +109 -0
- data/lib/generators/trek/templates/public_accounts/spec/web/accounts_spec.rb.tt +126 -0
- data/lib/generators/trek/templates/public_accounts/spec/web/authentication_spec.rb.tt +43 -0
- data/lib/generators/trek/templates/public_accounts/spec/web/sign_ins_spec.rb.tt +215 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.en.html.slim.tt +10 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.en.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.fr.html.slim.tt +13 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.fr.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.en.html.slim.tt +9 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.en.text.erb.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.fr.html.slim.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.fr.text.erb.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.en.html.slim.tt +10 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.en.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.fr.html.slim.tt +10 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.fr.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.en.html.slim.tt +10 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.en.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.fr.html.slim.tt +13 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.fr.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.en.html.slim.tt +9 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.en.text.erb.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.fr.html.slim.tt +9 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.fr.text.erb.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.en.html.slim.tt +23 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.en.text.erb.tt +14 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.fr.html.slim.tt +24 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.fr.text.erb.tt +14 -0
- data/lib/generators/trek/templates/public_accounts/web/authentication.rb.tt +97 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/accounts_controller.rb.tt +33 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/active_sessions_controller.rb.tt +15 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/base_controller.rb.tt +52 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/email_changes_controller.rb.tt +85 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/email_confirmations_controller.rb.tt +19 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/password_resets_controller.rb.tt +41 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/passwords_controller.rb.tt +23 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/registrations_controller.rb.tt +24 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/sessions_controller.rb.tt +78 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/sign_in_codes_controller.rb.tt +27 -0
- data/lib/generators/trek/templates/public_accounts/web/views/_flash.html.slim.tt +5 -0
- data/lib/generators/trek/templates/public_accounts/web/views/accounts/show.html.slim.tt +52 -0
- data/lib/generators/trek/templates/public_accounts/web/views/email_changes/edit.html.slim.tt +17 -0
- data/lib/generators/trek/templates/public_accounts/web/views/email_changes/new.html.slim.tt +15 -0
- data/lib/generators/trek/templates/public_accounts/web/views/password_resets/edit.html.slim.tt +26 -0
- data/lib/generators/trek/templates/public_accounts/web/views/password_resets/new.html.slim.tt +15 -0
- data/lib/generators/trek/templates/public_accounts/web/views/passwords/edit.html.slim.tt +25 -0
- data/lib/generators/trek/templates/public_accounts/web/views/registrations/new.html.slim.tt +28 -0
- data/lib/generators/trek/templates/public_accounts/web/views/sessions/new.html.slim.tt +38 -0
- data/lib/generators/trek/templates/public_accounts/web/views/sign_in_codes/new.html.slim.tt +17 -0
- data/lib/generators/trek/templates/spec/jobs/mail_delivery_job_spec.rb +11 -0
- data/lib/generators/trek/templates/spec/mailers/admin_mailer_spec.rb +10 -0
- data/lib/generators/trek/templates/spec/requests/admin/fragments_spec.rb +1 -1
- data/lib/generators/trek/templates/spec/requests/admin/good_job_spec.rb +34 -0
- data/lib/generators/trek/templates/spec/requests/admin/pages_spec.rb +1 -1
- data/lib/generators/trek/templates/spec/requests/admin/panels_spec.rb +16 -0
- data/lib/generators/trek/templates/spec/requests/admin/password_renewal_spec.rb +2 -2
- data/lib/generators/trek/templates/spec/requests/admin/sign_in_lockout_spec.rb +1 -1
- data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +2 -2
- data/lib/generators/trek/templates/views/admin/users/index.html.slim +1 -1
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.en.html.slim +1 -1
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.en.text.erb +1 -1
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.fr.html.slim +1 -1
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.fr.text.erb +1 -1
- data/lib/trek/csp_reports.rb +22 -4
- data/lib/trek/typography_formatter.rb +35 -4
- data/lib/trek/version.rb +1 -1
- data/package.json +12 -8
- data/trek.gemspec +1 -1
- data/vitest.config.mjs +24 -0
- data/yarn.lock +1004 -288
- metadata +90 -3
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
require "rails/generators/named_base"
|
|
2
|
+
require "rails/generators/migration"
|
|
3
|
+
require "generators/trek/helpers"
|
|
4
|
+
|
|
5
|
+
module Trek
|
|
6
|
+
module Generators
|
|
7
|
+
# Accounts for the site's or the mobile app's users (customers, members…), apart from the admin's
|
|
8
|
+
# User: their own model, table, sessions and sign-in, so nothing public can reach a staff account.
|
|
9
|
+
#
|
|
10
|
+
# They sign in one way, picked with --sign-in:
|
|
11
|
+
#
|
|
12
|
+
# - `code` (default): a code emailed to them, which creates the account the first time;
|
|
13
|
+
# - `password`: a password chosen on signing up, the email confirmed by a link, and reset by one.
|
|
14
|
+
#
|
|
15
|
+
# By default for a website: pages, and sessions kept in a cookie. With --api, for a mobile app:
|
|
16
|
+
# JSON endpoints, and sessions held through a JWT instead. With --model-only, neither: the models,
|
|
17
|
+
# mails and specs, for an app whose sign-in is its own (e.g. an API ported from another app).
|
|
18
|
+
class PublicAccountsGenerator < Rails::Generators::NamedBase
|
|
19
|
+
# Taken by the admin's account and Trek's own models, or planned for Trek 3 (Staff::User).
|
|
20
|
+
RESERVED_NAMES = %w[User Staff Session Current Page PagePath PageVersion Fragment MenuNode Tag Tagging TagCategory
|
|
21
|
+
ExternalLink AuditLog Dashboard].freeze
|
|
22
|
+
SIGN_INS = %w[code password].freeze
|
|
23
|
+
JWT_VERSION = "~> 3.1".freeze
|
|
24
|
+
|
|
25
|
+
include Rails::Generators::Migration
|
|
26
|
+
include Trek::Generators::Helpers
|
|
27
|
+
|
|
28
|
+
desc "Create accounts for the site's or the app's users, apart from the admin's: " \
|
|
29
|
+
"rails g trek:public_accounts Customer [--sign-in=code|password] [--api] [--path=customers] [--model-only]"
|
|
30
|
+
|
|
31
|
+
class_option :sign_in, type: :string, default: "code", enum: SIGN_INS,
|
|
32
|
+
desc: "How accounts sign in: with a code emailed to them, or with a password"
|
|
33
|
+
class_option :api, type: :boolean, default: false,
|
|
34
|
+
desc: "JSON endpoints and JWTs for a mobile app, instead of pages and a cookie"
|
|
35
|
+
class_option :path, type: :string, default: nil,
|
|
36
|
+
desc: "A path to put the routes under, e.g. members for /members/sign-in"
|
|
37
|
+
class_option :model_only, type: :boolean, default: false,
|
|
38
|
+
desc: "Only the models, mails and specs: no controllers, pages, routes nor tokens, the app signs in its own way"
|
|
39
|
+
|
|
40
|
+
source_root File.expand_path("templates/public_accounts", __dir__)
|
|
41
|
+
|
|
42
|
+
def check_name
|
|
43
|
+
return unless class_name.include?("::") || RESERVED_NAMES.include?(class_name)
|
|
44
|
+
|
|
45
|
+
raise Thor::Error, "#{class_name} can't be a public account model: pick a top-level name of the app's domain " \
|
|
46
|
+
"(Customer, Member, Reader…), other than #{RESERVED_NAMES.to_sentence(last_word_connector: " or ")}."
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def check_options
|
|
50
|
+
return unless model_only? && (api? || options[:path].present?)
|
|
51
|
+
|
|
52
|
+
raise Thor::Error, "--model-only adds no endpoints nor routes: drop --api and --path."
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def add_jwt_gem
|
|
56
|
+
return unless api?
|
|
57
|
+
return if in_gemfile?("jwt")
|
|
58
|
+
|
|
59
|
+
gem "jwt", JWT_VERSION
|
|
60
|
+
bundle_install
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def copy_models
|
|
64
|
+
template "models/account.rb.tt", "app/models/#{file_name}.rb"
|
|
65
|
+
template "models/account/session.rb.tt", "app/models/#{file_name}/session.rb"
|
|
66
|
+
template "models/account/sign_in_code.rb.tt", "app/models/#{file_name}/sign_in_code.rb" if code?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def copy_migration
|
|
70
|
+
return unless defined?(ActiveRecord)
|
|
71
|
+
|
|
72
|
+
migration_template "migrations/create_accounts.rb.tt", "db/migrate/trek_create_#{table_name}.rb",
|
|
73
|
+
migration_class_name:
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def add_current_attributes
|
|
77
|
+
inject_into_class "app/models/current.rb", "Current", <<-RUBY
|
|
78
|
+
# The signed-in #{human_name.downcase} (#{model_only? ? "set by the app's own sign-in" : "#{accounts_module}::Authentication"})
|
|
79
|
+
attribute :#{file_name}_session
|
|
80
|
+
delegate :#{file_name}, to: :#{file_name}_session, allow_nil: true
|
|
81
|
+
|
|
82
|
+
RUBY
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def copy_authentication_concern
|
|
86
|
+
return if model_only?
|
|
87
|
+
|
|
88
|
+
template "#{mode}/authentication.rb.tt", "app/controllers/concerns/#{accounts_path}/authentication.rb"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Every page of the site may show who is signed in. The API's controllers include it in their
|
|
92
|
+
# base controller instead.
|
|
93
|
+
def include_authentication_in_application_controller
|
|
94
|
+
return unless web?
|
|
95
|
+
|
|
96
|
+
inject_into_class "app/controllers/application_controller.rb", "ApplicationController",
|
|
97
|
+
" include #{accounts_module}::Authentication\n"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def copy_controllers
|
|
101
|
+
return if model_only?
|
|
102
|
+
|
|
103
|
+
controllers.each do |controller|
|
|
104
|
+
template "#{mode}/controllers/#{controller}_controller.rb.tt", "app/controllers/#{controllers_path}/#{controller}_controller.rb"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def copy_views
|
|
109
|
+
return unless web?
|
|
110
|
+
|
|
111
|
+
template "web/views/_flash.html.slim.tt", "app/views/#{controllers_path}/_flash.html.slim"
|
|
112
|
+
views.each do |view|
|
|
113
|
+
template "web/views/#{view}.html.slim.tt", "app/views/#{controllers_path}/#{view}.html.slim"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def copy_job
|
|
118
|
+
template "jobs/sign_in_code_job.rb.tt", "app/jobs/#{accounts_path}/sign_in_code_job.rb" if code?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def copy_mailer
|
|
122
|
+
template "mailers/account_mailer.rb.tt", "app/mailers/#{file_name}_mailer.rb"
|
|
123
|
+
mails.product(%w[en fr], %w[html.slim text.erb]).each do |mail, locale, format|
|
|
124
|
+
template "views/mailer/#{mail}.#{locale}.#{format}.tt", "app/views/#{file_name}_mailer/#{mail}.#{locale}.#{format}"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def copy_locales
|
|
129
|
+
%w[en fr].each do |locale|
|
|
130
|
+
template "locales/accounts.#{locale}.yml.tt", "config/locales/#{accounts_path}.#{locale}.yml"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def add_routes
|
|
135
|
+
route routes unless model_only?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def copy_specs
|
|
139
|
+
return unless File.directory?(File.join(destination_root, "spec"))
|
|
140
|
+
|
|
141
|
+
template "spec/models/account_spec.rb.tt", "spec/models/#{file_name}_spec.rb"
|
|
142
|
+
template "spec/models/account/sign_in_code_spec.rb.tt", "spec/models/#{file_name}/sign_in_code_spec.rb" if code?
|
|
143
|
+
return if model_only?
|
|
144
|
+
|
|
145
|
+
template "spec/#{mode}/sign_ins_spec.rb.tt", "spec/requests/#{controllers_path}/sign_ins_spec.rb"
|
|
146
|
+
template "spec/#{mode}/accounts_spec.rb.tt", "spec/requests/#{controllers_path}/accounts_spec.rb"
|
|
147
|
+
return if api?
|
|
148
|
+
|
|
149
|
+
template "spec/web/authentication_spec.rb.tt", "spec/controllers/application_controller_#{accounts_path}_spec.rb"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def code? = options[:sign_in] == "code"
|
|
155
|
+
def password? = options[:sign_in] == "password"
|
|
156
|
+
def api? = options[:api]
|
|
157
|
+
def model_only? = options[:model_only]
|
|
158
|
+
# Pages and a cookie: neither --api nor --model-only
|
|
159
|
+
def web? = !api? && !model_only?
|
|
160
|
+
def mode = api? ? "api" : "web"
|
|
161
|
+
# With trek:install:audit_log, which the account controllers write to
|
|
162
|
+
def audit_log? = File.exist?(File.join(destination_root, "app/models/audit_log.rb"))
|
|
163
|
+
|
|
164
|
+
# Customers: the module of the authentication concern, and the folder of its files.
|
|
165
|
+
def accounts_module = class_name.pluralize
|
|
166
|
+
def accounts_path = file_name.pluralize
|
|
167
|
+
|
|
168
|
+
# The controllers' module and folder: under Api::V1:: for the API, the first version of it, so that a
|
|
169
|
+
# later version can be served next to it while older releases of the mobile app still use this one.
|
|
170
|
+
def controllers_module = api? ? "Api::V1::#{accounts_module}" : accounts_module
|
|
171
|
+
def controllers_path = api? ? "api/v1/#{accounts_path}" : accounts_path
|
|
172
|
+
|
|
173
|
+
# The route helpers' prefix: customer_sign_in_path, api_v1_customer_sign_in_path…
|
|
174
|
+
def route_prefix = api? ? "api_v1_#{file_name}" : file_name
|
|
175
|
+
|
|
176
|
+
# Where the routes go: /sign-in, /members/sign-in, /api/v1/sign-in…
|
|
177
|
+
def routes_path = [("api/v1" if api?), options[:path].presence].compact.join("/").presence
|
|
178
|
+
|
|
179
|
+
def controllers
|
|
180
|
+
names = code? ? %w[sign_in_codes sessions] : %w[registrations sessions password_resets email_confirmations passwords]
|
|
181
|
+
%w[base] + names + %w[accounts email_changes active_sessions]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def views
|
|
185
|
+
shared = %w[sessions/new accounts/show email_changes/new]
|
|
186
|
+
if code?
|
|
187
|
+
shared + %w[sign_in_codes/new email_changes/edit]
|
|
188
|
+
else
|
|
189
|
+
shared + %w[registrations/new password_resets/new password_resets/edit passwords/edit]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def mails
|
|
194
|
+
code? ? %w[sign_in_code email_change_code] : %w[email_confirmation password_reset email_change sign_in_locked]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# On paths of their own rather than the model's name, so renaming the model keeps them.
|
|
198
|
+
def routes
|
|
199
|
+
scope = [(%("#{routes_path}") if routes_path), "module: \"#{controllers_path}\"", "as: :#{route_prefix}"].compact
|
|
200
|
+
scope << "defaults: { format: :json }" if api?
|
|
201
|
+
|
|
202
|
+
<<~RUBY
|
|
203
|
+
# #{human_name.pluralize}' accounts (#{accounts_module}::Authentication)
|
|
204
|
+
scope #{scope.join(", ")} do
|
|
205
|
+
#{(api? ? api_routes : web_routes).gsub(/^/, " ")}end
|
|
206
|
+
RUBY
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def web_routes
|
|
210
|
+
sign_in = if code?
|
|
211
|
+
<<~RUBY
|
|
212
|
+
get "sign-in", to: "sign_in_codes#new", as: :sign_in
|
|
213
|
+
post "sign-in", to: "sign_in_codes#create"
|
|
214
|
+
get "sign-in/code", to: "sessions#new", as: :sign_in_code
|
|
215
|
+
post "sign-in/code", to: "sessions#create"
|
|
216
|
+
RUBY
|
|
217
|
+
else
|
|
218
|
+
<<~RUBY
|
|
219
|
+
get "sign-in", to: "sessions#new", as: :sign_in
|
|
220
|
+
post "sign-in", to: "sessions#create"
|
|
221
|
+
get "sign-up", to: "registrations#new", as: :sign_up
|
|
222
|
+
post "sign-up", to: "registrations#create"
|
|
223
|
+
resource :password_reset, only: %i[new create edit update], path: "password-reset"
|
|
224
|
+
get "account/confirm", to: "email_confirmations#show", as: :email_confirmation
|
|
225
|
+
post "account/confirm", to: "email_confirmations#create"
|
|
226
|
+
resource :password, only: %i[edit update], path: "account/password"
|
|
227
|
+
RUBY
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
sign_in + <<~RUBY
|
|
231
|
+
delete "sign-out", to: "sessions#destroy", as: :sign_out
|
|
232
|
+
resource :account, only: %i[show update destroy]
|
|
233
|
+
resource :email_change, only: %i[new create #{code? ? "edit update" : "show"}], path: "account/email"
|
|
234
|
+
resources :active_sessions, only: :destroy, path: "account/sessions" do
|
|
235
|
+
delete :others, action: :destroy_others, on: :collection
|
|
236
|
+
end
|
|
237
|
+
RUBY
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def api_routes
|
|
241
|
+
sign_in = if code?
|
|
242
|
+
<<~RUBY
|
|
243
|
+
post "sign-in", to: "sign_in_codes#create", as: :sign_in
|
|
244
|
+
post "sign-in/code", to: "sessions#create", as: :sign_in_code
|
|
245
|
+
RUBY
|
|
246
|
+
else
|
|
247
|
+
<<~RUBY
|
|
248
|
+
post "sign-in", to: "sessions#create", as: :sign_in
|
|
249
|
+
post "sign-up", to: "registrations#create", as: :sign_up
|
|
250
|
+
resource :password_reset, only: %i[create update], path: "password-reset"
|
|
251
|
+
resource :email_confirmation, only: %i[create update], path: "account/confirmation"
|
|
252
|
+
resource :password, only: :update, path: "account/password"
|
|
253
|
+
RUBY
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
sign_in + <<~RUBY
|
|
257
|
+
delete "session", to: "sessions#destroy", as: :session
|
|
258
|
+
resource :account, only: %i[show update destroy]
|
|
259
|
+
resource :email_change, only: %i[create update], path: "account/email"
|
|
260
|
+
resources :active_sessions, only: %i[index destroy], path: "account/sessions" do
|
|
261
|
+
delete :others, action: :destroy_others, on: :collection
|
|
262
|
+
end
|
|
263
|
+
RUBY
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require "rails/generators/actions"
|
|
2
|
+
require "generators/trek/helpers"
|
|
3
|
+
|
|
4
|
+
module Trek
|
|
5
|
+
module Generators
|
|
6
|
+
class ScalingoGenerator < Rails::Generators::Base
|
|
7
|
+
BUILDPACK_URL = "https://github.com/etaminstudio/trek-buildpack/archive/refs/tags/v0.1.0.tar.gz".freeze
|
|
8
|
+
|
|
9
|
+
include Trek::Generators::Helpers
|
|
10
|
+
|
|
11
|
+
desc "Create Scalingo files."
|
|
12
|
+
|
|
13
|
+
# The trek-buildpack replaces the apt + ruby buildpack combination. It
|
|
14
|
+
# pins Node from the .node-version that `rails new` already generates.
|
|
15
|
+
def remove_buildpacks
|
|
16
|
+
remove_file ".buildpacks"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create_aptfile
|
|
20
|
+
create_file "Aptfile" do
|
|
21
|
+
<<~TEXT
|
|
22
|
+
libjemalloc2
|
|
23
|
+
libvips-tools --no-install-recommends
|
|
24
|
+
libwebp-dev
|
|
25
|
+
TEXT
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create_procfile
|
|
30
|
+
create_file "Procfile" do
|
|
31
|
+
<<~TEXT
|
|
32
|
+
web: bundle exec rails server -p $PORT -e $RAILS_ENV -b 0.0.0.0
|
|
33
|
+
# A worker is only needed once jobs outgrow the web container
|
|
34
|
+
# (config.good_job.execution_mode = :async):
|
|
35
|
+
# worker: bundle exec good_job start
|
|
36
|
+
postdeploy: bundle exec rails db:migrate
|
|
37
|
+
TEXT
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def final_instructions
|
|
42
|
+
say ""
|
|
43
|
+
say "Configure the Scalingo app environment:"
|
|
44
|
+
say ""
|
|
45
|
+
say " scalingo --app <app> env-set BUILDPACK_URL=#{BUILDPACK_URL}"
|
|
46
|
+
say " scalingo --app <app> env-set 'DATABASE_URL=$SCALINGO_POSTGRESQL_URL'"
|
|
47
|
+
say ""
|
|
48
|
+
say "and make sure config/database.yml reads DATABASE_URL in production."
|
|
49
|
+
say ""
|
|
50
|
+
say "See https://trek.etamin.studio/deploy for details."
|
|
51
|
+
say ""
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Admin
|
|
2
|
+
# After a suspected compromise: every user, this admin included, must choose a new password at
|
|
3
|
+
# their next request (User.require_password_renewal!).
|
|
4
|
+
class PasswordRenewalsController < Admin::BaseController
|
|
5
|
+
def create
|
|
6
|
+
authorize! User, to: :require_password_renewal?
|
|
7
|
+
User.require_password_renewal!
|
|
8
|
+
redirect_to admin_users_path, notice: t(".success")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -24,14 +24,6 @@ module Admin
|
|
|
24
24
|
@object.password = SecureRandom.alphanumeric(24) if @object.password.blank?
|
|
25
25
|
end
|
|
26
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
|
-
|
|
35
27
|
def theme
|
|
36
28
|
value = params[:theme].to_s
|
|
37
29
|
Current.user.update!(theme: value) if User.themes.key?(value)
|
|
@@ -22,8 +22,9 @@ Rails.application.configure do
|
|
|
22
22
|
# Framed by its own pages only (Lookbook's previews), against clickjacking
|
|
23
23
|
policy.frame_ancestors :self
|
|
24
24
|
policy.object_src :none
|
|
25
|
-
# No inline script: the app's own files, and the scripts given the request's nonce
|
|
26
|
-
|
|
25
|
+
# No inline script: the app's own files, and the scripts given the request's nonce. A blocked
|
|
26
|
+
# inline script's first 40 characters go with the report (`'report-sample'`), to tell what it was
|
|
27
|
+
policy.script_src :self, :report_sample
|
|
27
28
|
# Inline `style` attributes stay allowed: Trek's components and Backpack's set some (colors,
|
|
28
29
|
# custom properties)
|
|
29
30
|
policy.style_src :self, :unsafe_inline
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Rails.application.configure do
|
|
2
|
+
# One scheduler for every queue. Give a queue its own lane when it must not wait behind the
|
|
3
|
+
# others, e.g. "mailers:2;*:3" — the thread counts have to stay within GOOD_JOB_MAX_THREADS,
|
|
4
|
+
# which config/database.yml turns into database connections.
|
|
5
|
+
config.good_job.queues = "*"
|
|
6
|
+
|
|
7
|
+
# Cron runs wherever a scheduler runs, which in :async mode means every web container. Keep it
|
|
8
|
+
# to production so a development machine never fires a scheduled job.
|
|
9
|
+
config.good_job.enable_cron = Rails.env.production?
|
|
10
|
+
config.good_job.cron = {
|
|
11
|
+
# purge_old_sessions: {
|
|
12
|
+
# cron: "0 3 * * *", # every day at 3am
|
|
13
|
+
# class: "PurgeOldSessionsJob",
|
|
14
|
+
# description: "Delete the user sessions that expired more than a month ago"
|
|
15
|
+
# }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# An error raised outside a job's own execution (in the scheduler thread) has no retry handler
|
|
19
|
+
# to catch it. Report it through Rails.error, like the rest of the app.
|
|
20
|
+
config.good_job.on_thread_error = ->(error) { Rails.error.report(error, handled: false) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The dashboard is mounted at /admin/good_job, outside Admin::BaseController and therefore outside
|
|
24
|
+
# Trek's authentication. Guard it with the same signed session cookie, and answer 404 rather than
|
|
25
|
+
# redirecting: the route stays invisible to anyone who is not an administrator.
|
|
26
|
+
ActiveSupport.on_load(:good_job_application_controller) do
|
|
27
|
+
before_action do
|
|
28
|
+
raise ActionController::RoutingError, "Not Found" unless good_job_dashboard_user&.admin?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def good_job_dashboard_user
|
|
34
|
+
return unless cookies.signed[:session_id]
|
|
35
|
+
|
|
36
|
+
<%= session_model %>.find_by(id: cookies.signed[:session_id])&.user
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Delivers every `deliver_later` email (config.action_mailer.delivery_job).
|
|
2
|
+
# GoodJob discards a failed job rather than retrying it, so a transient error from the delivery
|
|
3
|
+
# service would lose the email: retry with a growing delay instead.
|
|
4
|
+
class MailDeliveryJob < ActionMailer::MailDeliveryJob
|
|
5
|
+
retry_on StandardError, wait: :polynomially_longer, attempts: 5
|
|
6
|
+
end
|
|
@@ -56,6 +56,11 @@ html(lang=I18n.locale class=class_for_theme)
|
|
|
56
56
|
href: %i[admin users],
|
|
57
57
|
allowed_to: allowed_to?(:index?, User),
|
|
58
58
|
)
|
|
59
|
+
s.with_item(
|
|
60
|
+
text: t("admin.jobs"),
|
|
61
|
+
href: "/admin/good_job",
|
|
62
|
+
allowed_to: Current.user.admin?,
|
|
63
|
+
)
|
|
59
64
|
end
|
|
60
65
|
end
|
|
61
66
|
= render Trek::ToasterComponent.new
|
|
@@ -13,11 +13,13 @@ class AdminMailer < ApplicationMailer
|
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
# A sign-in from a browser that never signed in as the user, so they can react if it wasn't them
|
|
16
|
+
# A sign-in from a browser that never signed in as the user, so they can react if it wasn't them.
|
|
17
|
+
# The IP address comes with its host name, as on the active sessions: sent later, the email can
|
|
18
|
+
# wait for the lookup.
|
|
17
19
|
def new_sign_in(user, at:, ip:, user_agent:)
|
|
18
20
|
@user = user
|
|
19
21
|
@at = at
|
|
20
|
-
@
|
|
22
|
+
@address = [ip, Trek::ReverseDns.hostname(ip)&.then { |hostname| "(#{hostname})" }].compact.join(" ")
|
|
21
23
|
@user_agent = user_agent
|
|
22
24
|
@sessions_url = admin_user_url(user, anchor: "sessions")
|
|
23
25
|
@reset_url = new_admin_user_password_reset_url
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Signs <%= human_name.pluralize.downcase %> in to the API through a JWT the mobile app sends with each request
|
|
2
|
+
# (Authorization: Bearer). The token only names a <%= class_name %>::Session: signing out deletes the session and
|
|
3
|
+
# the token with it, and the session's idle timeout and lifetime apply as on the web. Apart from the
|
|
4
|
+
# admin's users and sessions altogether.
|
|
5
|
+
module <%= accounts_module %>::Authentication
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
before_action :require_<%= file_name %>
|
|
10
|
+
after_action :track_<%= file_name %>_activity
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class_methods do
|
|
14
|
+
def allow_unauthenticated_access(**options)
|
|
15
|
+
skip_before_action :require_<%= file_name %>, **options
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def require_<%= file_name %>
|
|
22
|
+
render_api_error :unauthorized, :sign_in_needed unless resume_<%= file_name %>_session
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Changing the password or deleting the account asks to sign in again when the session isn't recent.
|
|
26
|
+
def require_recent_<%= file_name %>_sign_in
|
|
27
|
+
render_api_error :forbidden, :recent_sign_in_needed unless Current.<%= file_name %>_session.recent?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def resume_<%= file_name %>_session
|
|
31
|
+
Current.<%= file_name %>_session ||= find_<%= file_name %>_session_by_token
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# A session past its idle timeout or lifetime is dropped: the <%= human_name.downcase %> signs in again.
|
|
35
|
+
def find_<%= file_name %>_session_by_token
|
|
36
|
+
token = request.authorization.to_s[/\ABearer (.+)\z/, 1]
|
|
37
|
+
return unless token
|
|
38
|
+
|
|
39
|
+
<%= file_name %>_session = <%= class_name %>::Session.from_token(token)
|
|
40
|
+
return <%= file_name %>_session unless <%= file_name %>_session&.expired?
|
|
41
|
+
|
|
42
|
+
<%= file_name %>_session.destroy
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The session the token of the response will name
|
|
47
|
+
def start_<%= file_name %>_session_for(<%= file_name %>)
|
|
48
|
+
<%= file_name %>.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |<%= file_name %>_session|
|
|
49
|
+
Current.<%= file_name %>_session = <%= file_name %>_session
|
|
50
|
+
Trek::ResolveSessionHostnameJob.perform_later(<%= file_name %>_session)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def terminate_<%= file_name %>_session
|
|
55
|
+
Current.<%= file_name %>_session.destroy
|
|
56
|
+
Current.<%= file_name %>_session = nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# At most once every <%= class_name %>::Session.activity_interval (5 minutes), so using the app doesn't write on
|
|
60
|
+
# every request.
|
|
61
|
+
def track_<%= file_name %>_activity
|
|
62
|
+
<%= file_name %>_session = Current.<%= file_name %>_session
|
|
63
|
+
return unless <%= file_name %>_session&.persisted?
|
|
64
|
+
|
|
65
|
+
<%= file_name %>_session.record_activity
|
|
66
|
+
<%= file_name %> = <%= file_name %>_session.<%= file_name %>
|
|
67
|
+
return if <%= file_name %>.last_activity_at&.after?(<%= class_name %>::Session.activity_interval.ago)
|
|
68
|
+
|
|
69
|
+
<%= file_name %>.update_columns(last_activity_at: Time.current) # rubocop:disable Rails/SkipsModelValidations
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# An error the app can act on (`error`), with a message it may show (`message`).
|
|
73
|
+
def render_api_error(status, error, **details)
|
|
74
|
+
render json: { error:, message: I18n.t("<%= accounts_path %>.api.errors.#{error}"), **details }, status:
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# The signed-in <%= human_name.downcase %>'s account: GET, PATCH { name }, and DELETE after a recent sign-in.
|
|
2
|
+
class <%= controllers_module %>::AccountsController < <%= controllers_module %>::BaseController
|
|
3
|
+
before_action :require_recent_<%= file_name %>_sign_in, only: :destroy
|
|
4
|
+
|
|
5
|
+
def show
|
|
6
|
+
render json: <%= file_name %>_json(Current.<%= file_name %>)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def update
|
|
10
|
+
if Current.<%= file_name %>.update(params.permit(:name))
|
|
11
|
+
render json: <%= file_name %>_json(Current.<%= file_name %>)
|
|
12
|
+
else
|
|
13
|
+
render_api_error :unprocessable_content, :invalid, errors: Current.<%= file_name %>.errors
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def destroy
|
|
18
|
+
Current.<%= file_name %>.destroy!
|
|
19
|
+
Current.<%= file_name %>_session = nil
|
|
20
|
+
head :no_content
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/generators/trek/templates/public_accounts/api/controllers/active_sessions_controller.rb.tt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# The devices signed in as the current <%= human_name.downcase %>: GET lists them, DELETE signs one or the others out.
|
|
2
|
+
# Only theirs: every query goes through Current.<%= file_name %>.sessions.
|
|
3
|
+
class <%= controllers_module %>::ActiveSessionsController < <%= controllers_module %>::BaseController
|
|
4
|
+
def index
|
|
5
|
+
sessions = Current.<%= file_name %>.sessions.active.recent_first.map do |<%= file_name %>_session|
|
|
6
|
+
<%= file_name %>_session.as_json(only: %i[id user_agent ip_address ip_hostname created_at last_activity_at])
|
|
7
|
+
.merge("current" => <%= file_name %>_session == Current.<%= file_name %>_session)
|
|
8
|
+
end
|
|
9
|
+
render json: sessions
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def destroy
|
|
13
|
+
Current.<%= file_name %>.sessions.find(params[:id]).destroy
|
|
14
|
+
head :no_content
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def destroy_others
|
|
18
|
+
Current.<%= file_name %>.sessions.where.not(id: Current.<%= file_name %>_session.id).destroy_all
|
|
19
|
+
head :no_content
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# The API's account endpoints: <%= human_name.pluralize.downcase %> only, never the admin's users.
|
|
2
|
+
class <%= controllers_module %>::BaseController < ActionController::API
|
|
3
|
+
include <%= accounts_module %>::Authentication
|
|
4
|
+
# Sign-ins, <% if code? %>code requests<% else %>sign-ups, password resets<% end %> and account changes go to the audit log, when the app has one.
|
|
5
|
+
include Trek::Auditable if defined?(::AuditLog)
|
|
6
|
+
|
|
7
|
+
# Emails sent to one address (<% if code? %>codes<% else %>password resets, confirmations<% end %>), whatever the IP address: rate_limit
|
|
8
|
+
# counts per IP address.
|
|
9
|
+
EMAILS_PER_ADDRESS = 5
|
|
10
|
+
EMAILS_PER_ADDRESS_WINDOW = 1.hour
|
|
11
|
+
|
|
12
|
+
# Failed sign-ins from one IP address, whatever the account, over a longer window than rate_limit's.
|
|
13
|
+
FAILED_SIGN_INS_PER_IP = 20
|
|
14
|
+
FAILED_SIGN_INS_WINDOW = 1.hour
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def normalized_email(email)
|
|
19
|
+
<%= class_name %>.normalize_value_for(:email, email.to_s)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def valid_email?(email)
|
|
23
|
+
email.match?(URI::MailTo::EMAIL_REGEXP)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Counted in Rails.cache, as rate_limit is, under a digest rather than the email itself.
|
|
27
|
+
def too_many_emails_to?(email)
|
|
28
|
+
key = "<%= accounts_path %>/emails/#{Digest::SHA256.hexdigest(email)}"
|
|
29
|
+
Rails.cache.increment(key, 1, expires_in: EMAILS_PER_ADDRESS_WINDOW).to_i > EMAILS_PER_ADDRESS
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def too_many_failed_sign_ins_from_ip?
|
|
33
|
+
Rails.cache.read(failed_sign_ins_from_ip_key).to_i >= FAILED_SIGN_INS_PER_IP
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def count_failed_sign_in_from_ip
|
|
37
|
+
Rails.cache.increment(failed_sign_ins_from_ip_key, 1, expires_in: FAILED_SIGN_INS_WINDOW)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def failed_sign_ins_from_ip_key
|
|
41
|
+
"<%= accounts_path %>/failed_sign_ins/#{request.remote_ip}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def render_rate_limited
|
|
45
|
+
render_api_error :too_many_requests, :rate_limited
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def <%= file_name %>_json(<%= file_name %>)
|
|
49
|
+
<% if password? -%>
|
|
50
|
+
<%= file_name %>.as_json(only: %i[id email unconfirmed_email name confirmed_at created_at])
|
|
51
|
+
<% else -%>
|
|
52
|
+
<%= file_name %>.as_json(only: %i[id email name created_at])
|
|
53
|
+
<% end -%>
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# A new session, and the token naming it, for the app to send with each request
|
|
57
|
+
def render_new_session(<%= file_name %>, status: :created)
|
|
58
|
+
<%= file_name %>_session = start_<%= file_name %>_session_for(<%= file_name %>)
|
|
59
|
+
render json: { token: <%= file_name %>_session.token, <%= file_name %>: <%= file_name %>_json(<%= file_name %>) }, status:
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# For the audit log (Trek::Auditable): the signed-in <%= human_name.downcase %>, nil before signing in
|
|
63
|
+
def audit_actor
|
|
64
|
+
Current.<%= file_name %>
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The account signed in to, tried, or changed
|
|
68
|
+
def audit_target
|
|
69
|
+
@<%= file_name %> || Current.<%= file_name %>
|
|
70
|
+
end
|
|
71
|
+
end
|