aikotoba 0.1.1 → 0.2.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +244 -13
  3. data/Rakefile +0 -2
  4. data/app/controllers/aikotoba/accounts_controller.rb +2 -2
  5. data/app/controllers/aikotoba/application_controller.rb +2 -1
  6. data/app/controllers/aikotoba/confirms_controller.rb +14 -14
  7. data/app/controllers/aikotoba/recoveries_controller.rb +14 -14
  8. data/app/controllers/aikotoba/sessions_controller.rb +3 -3
  9. data/app/controllers/aikotoba/unlocks_controller.rb +14 -14
  10. data/app/controllers/concerns/aikotoba/authenticatable.rb +93 -17
  11. data/app/controllers/concerns/aikotoba/protection/rate_limiting.rb +32 -0
  12. data/app/controllers/concerns/aikotoba/scopable.rb +36 -0
  13. data/app/models/aikotoba/account/{service/confirmation.rb → confirmation.rb} +1 -1
  14. data/app/models/aikotoba/account/confirmation_token.rb +2 -1
  15. data/app/models/aikotoba/account/{service/lock.rb → lock.rb} +1 -1
  16. data/app/models/aikotoba/account/password.rb +1 -1
  17. data/app/models/aikotoba/account/{service/recovery.rb → recovery.rb} +1 -1
  18. data/app/models/aikotoba/account/recovery_token.rb +2 -1
  19. data/app/models/aikotoba/account/session.rb +40 -0
  20. data/app/models/aikotoba/account/token.rb +2 -2
  21. data/app/models/aikotoba/account/unlock_token.rb +2 -1
  22. data/app/models/aikotoba/account.rb +68 -10
  23. data/app/models/concerns/aikotoba/token_encryptable.rb +1 -1
  24. data/app/views/aikotoba/accounts/new.html.erb +4 -4
  25. data/app/views/aikotoba/confirms/new.html.erb +3 -3
  26. data/app/views/aikotoba/recoveries/edit.html.erb +3 -3
  27. data/app/views/aikotoba/recoveries/new.html.erb +3 -3
  28. data/app/views/aikotoba/sessions/_links.html.erb +4 -4
  29. data/app/views/aikotoba/sessions/new.html.erb +3 -3
  30. data/app/views/aikotoba/unlocks/new.html.erb +3 -3
  31. data/config/locales/en.yml +5 -6
  32. data/config/routes.rb +30 -22
  33. data/db/migrate/20211204121532_create_aikotoba_accounts.rb +11 -0
  34. data/lib/aikotoba/version.rb +1 -1
  35. data/lib/aikotoba.rb +58 -18
  36. metadata +9 -13
  37. data/app/controllers/concerns/aikotoba/protection/timing_atack.rb +0 -23
  38. data/app/models/aikotoba/account/service/authentication.rb +0 -66
  39. data/app/models/aikotoba/account/service/registration.rb +0 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 173aca2780a3086a9276f56c4bd59a1c4aedb6aa2a8f2d3e29068f00e8a5f417
4
- data.tar.gz: d96bea3b841912e7586c07a984e58c6b0daea49109a3b73bc3cc90090cf7434f
3
+ metadata.gz: a8e76b0ed9268bcddffdb90924b637de6f65effe09b3da217dde7c26bbe97127
4
+ data.tar.gz: 363e1aea2df7ba058d4cde72c17cfe78bb51bbdc52d91f14ccd8594304d938b0
5
5
  SHA512:
6
- metadata.gz: 050d2f984527212f1a0426fd830c0b0ae1dfff51ea3e75fda1a5596cc702175996f70c3bbf1ca9acf6b21ea2099863ee5dfa705654db8ad338053ce384aeb635
7
- data.tar.gz: 7efcedd94cb09d06f9d846d6d43931ded9eb243dd7625f5a6ab74a41682082ca2d2b4c00feb27a1c282006ab04e44b70086d7024554ac4fe7e73cae273369f93
6
+ metadata.gz: 1ddf99bfd428b5bf537f36933413809fac6451eef82fb7becc62830fa38c3c478f9008269c8b83969d3974c5c7ae9a7041199dd8739ac1c604050ef424decc2e
7
+ data.tar.gz: cdbcd7e23e21a87bdd36ede42d74f0c446ecc8da75c1a48bac54e54a42ca00179e9cc7d9fde945f990200673d638ee7c5084d374379ab019be4666a80f6d867d
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  [![CI](https://github.com/madogiwa0124/aikotoba/actions/workflows/ci.yml/badge.svg)](https://github.com/madogiwa0124/aikotoba/actions/workflows/ci.yml)
2
+ [![Gem Version](https://badge.fury.io/rb/aikotoba.svg)](https://badge.fury.io/rb/aikotoba)
2
3
 
3
4
  # Aikotoba
4
5
 
@@ -152,6 +153,76 @@ Aikotoba enables a route to recover an account by password reset.
152
153
  | GET | /recover/:token | Display page for recover account by password reset. |
153
154
  | PATCH | /recover/:token | Recover account by password reset. |
154
155
 
156
+ ### Rate Limiting (Rails 8+ only)
157
+
158
+ Aikotoba provides built-in rate limiting for email-sending endpoints to prevent email bombing attacks. This feature requires **Rails 8.0 or later**.
159
+
160
+ Rate limiting is available for:
161
+
162
+ - **Confirmation token requests** (`/confirm` POST endpoint)
163
+ - **Unlock token requests** (`/unlock` POST endpoint)
164
+ - **Password recovery token requests** (`/recover` POST endpoint)
165
+
166
+ By default, rate limiting is disabled (empty configuration). To enable it, configure the respective options:
167
+
168
+ ```ruby
169
+ Aikotoba.confirmation_rate_limit_options = {
170
+ to: 10,
171
+ within: 1.hour,
172
+ by: -> { request.params.dig(:account, :email).presence || request.remote_ip },
173
+ only: :create
174
+ }
175
+ ```
176
+
177
+ When rate limiting is triggered, requests that exceed the limit will receive a 429 (Too Many Requests) response.
178
+
179
+ For detailed configuration examples and options, see the [Configuration](#configuration) section below.
180
+
181
+ ### Multiple Scopes
182
+
183
+ Aikotoba supports multiple scopes.
184
+
185
+ You can add a scope by `Aikotoba.add_scope` method. For example, the following code adds an `admin` scope. Unspecified values will be copied from the `default` scope.
186
+
187
+ ```ruby
188
+ Aikotoba.add_scope(:admin, {
189
+ session_key: "aikotoba_admin_session_token",
190
+ root_path: "/admin",
191
+ sign_in_path: "/admin/sign_in",
192
+ sign_up_path: "/admin/sign_up",
193
+ after_sign_in_path: "/admin/sensitives",
194
+ after_sign_out_path: "/admin/sign_in",
195
+ sign_out_path: "/admin/sign_out",
196
+ confirm_path: "/admin/confirm",
197
+ unlock_path: "/admin/unlock",
198
+ recover_path: "/admin/recover"
199
+ })
200
+ ```
201
+
202
+ As shown below, you can perform separate authentication with `/sign_in` and `/admin/sign_in`.
203
+
204
+ ```sh
205
+ $ bin/rails routes
206
+ Routes for Aikotoba::Engine:
207
+ Prefix Verb URI Pattern Controller#Action
208
+ new_session GET /sign_in(.:format) aikotoba/sessions#new
209
+ admin_new_session GET /admin/sign_in(.:format) aikotoba/sessions#new
210
+ ```
211
+
212
+ The scope is determined dynamically by the root path. For example, when accessing `/admin/sign_in`, the `admin` scope is selected.
213
+
214
+ To automatically switch scopes and get paths, use the `Aikotoba::Scopable#aikotoba_scoped_path` helper method.
215
+
216
+ ```ruby
217
+ include Aikotoba::Scopable
218
+
219
+ helper_method :aikotoba_scoped_path
220
+
221
+ aikotoba_scoped_path(:new_session)
222
+ #=> "/sign_in" (if current scope is :default)
223
+ #=> "/admin/sign_in" (if current scope is :admin)
224
+ ```
225
+
155
226
  ## Configuration
156
227
 
157
228
  The following configuration parameters are supported. You can override it. (ex. `initializers/aikotoba.rb`)
@@ -159,37 +230,177 @@ The following configuration parameters are supported. You can override it. (ex.
159
230
  ```ruby
160
231
  require 'aikotoba'
161
232
 
233
+ # ============================================
234
+ # Global settings
235
+ # ============================================
236
+
162
237
  Aikotoba.parent_controller = "ApplicationController"
163
238
  Aikotoba.parent_mailer = "ActionMailer::Base"
164
239
  Aikotoba.mailer_sender = "from@example.com"
165
240
  Aikotoba.email_format = /\A[^\s]+@[^\s]+\z/
166
- Aikotoba.prevent_timing_atack = true
167
241
  Aikotoba.password_pepper = "aikotoba-default-pepper"
168
242
  Aikotoba.password_length_range = 8..100
169
- Aikotoba.sign_in_path = "/sign_in"
170
- Aikotoba.sign_out_path = "/sign_out"
171
- Aikotoba.after_sign_in_path = "/"
172
- Aikotoba.after_sign_out_path = "/sign_in"
243
+ Aikotoba.session_expiry = 7.days
244
+
173
245
 
174
246
  # for registerable
175
247
  Aikotoba.registerable = true
176
- Aikotoba.sign_up_path = "/sign_up"
177
248
 
178
249
  # for confirmable
179
250
  Aikotoba.confirmable = false
180
- Aikotoba.confirm_path = "/confirm"
181
251
  Aikotoba.confirmation_token_expiry = 1.day
182
252
 
183
253
  # for lockable
184
254
  Aikotoba.lockable = false
185
- Aikotoba.unlock_path = "/unlock"
186
255
  Aikotoba.max_failed_attempts = 10
187
256
  Aikotoba.unlock_token_expiry = 1.day
188
257
 
189
258
  # for Recoverable
190
259
  Aikotoba.recoverable = false
191
- Aikotoba.recover_path = "/recover"
192
260
  Aikotoba.recovery_token_expiry = 4.hours
261
+
262
+ # ============================================
263
+ # Rate Limiting (Rails 8+ required)
264
+ # ============================================
265
+ # Rate limiting protects email-sending endpoints (confirm, unlock, recover) from email bombing attacks.
266
+ # Default (empty hash): no rate limiting
267
+
268
+ # Requires Rails 8.0+ to use the built-in rate_limit feature.
269
+ #
270
+ # Configuration format: { to: <max_requests>, within: <time_duration>, by: <identifier_proc>, only: <action> }
271
+ #
272
+ # SECURITY RECOMMENDATION:
273
+ # Use .dig() with fallback to prevent nil errors and ensure rate limiting always works:
274
+ # by: -> { request.params.dig(:account, :email).presence || request.remote_ip }
275
+ #
276
+ # This ensures:
277
+ # - Invalid or missing email params don't bypass rate limiting
278
+ # - Fallback to IP address when email is not provided
279
+ # - Protection against email enumeration attacks
280
+ #
281
+ # Examples:
282
+
283
+ # Limit confirmation token requests to 10 per hour, per email address (with IP fallback)
284
+ Aikotoba.confirmation_rate_limit_options = {
285
+ to: 10,
286
+ within: 1.hour,
287
+ by: -> { request.params.dig(:account, :email).presence || request.remote_ip },
288
+ only: :create
289
+ }
290
+
291
+ # Limit unlock token requests to 5 per hour, per email address (stricter for security)
292
+ Aikotoba.unlock_rate_limit_options = {
293
+ to: 5,
294
+ within: 1.hour,
295
+ by: -> { request.params.dig(:account, :email).presence || request.remote_ip },
296
+ only: :create
297
+ }
298
+
299
+ # Limit recovery token requests to 3 per hour, per email address (most strict for password recovery)
300
+ Aikotoba.recovery_rate_limit_options = {
301
+ to: 3,
302
+ within: 1.hour,
303
+ by: -> { request.params.dig(:account, :email).presence || request.remote_ip },
304
+ only: :create
305
+ }
306
+
307
+ # Rate limiting by IP address only (simpler, but less precise)
308
+ # Aikotoba.confirmation_rate_limit_options = {
309
+ # to: 20,
310
+ # within: 1.hour,
311
+ # by: -> { request.remote_ip },
312
+ # only: :create
313
+ # }
314
+
315
+ # ============================================
316
+ # Scope settings
317
+ # ============================================
318
+
319
+ # for Default Scope
320
+ # You can override only the necessary keys.
321
+ Aikotoba.default_scope = {
322
+ authenticate_for: nil, # No restriction for default scope
323
+ session_key: "aikotoba_session_token",
324
+ root_path: "/",
325
+ sign_in_path: "/sign_in",
326
+ sign_out_path: "/sign_out",
327
+ sign_up_path: "/sign_up",
328
+ confirm_path: "/confirm",
329
+ unlock_path: "/unlock",
330
+ recover_path: "/recover",
331
+ after_sign_in_path: "/sensitives",
332
+ after_sign_out_path: "/sign_in"
333
+ }
334
+
335
+ # for Additional Scopes
336
+ Aikotoba.add_scope(:admin, {
337
+ authenticate_for: "Admin", # Restrict authentication to Admin accounts
338
+ session_key: "aikotoba_admin_session_token",
339
+ root_path: "/admin",
340
+ sign_in_path: "/admin/sign_in",
341
+ sign_out_path: "/admin/sign_out",
342
+ sign_up_path: "/admin/sign_up",
343
+ confirm_path: "/admin/confirm",
344
+ unlock_path: "/admin/unlock",
345
+ recover_path: "/admin/recover",
346
+ after_sign_in_path: "/admin/sensitives",
347
+ after_sign_out_path: "/admin/sign_in"
348
+ })
349
+ ```
350
+
351
+ ### Scope Configuration Details
352
+
353
+ `Aikotoba.default_scope=` **merges** the provided hash into the existing default scope (does not replace):
354
+
355
+ ```ruby
356
+ # Single key update
357
+ Aikotoba.default_scope[:sign_in_path] = "/custom"
358
+
359
+ # Multiple keys update (recommended for bulk changes)
360
+ Aikotoba.default_scope = {
361
+ sign_in_path: "/custom_sign_in",
362
+ after_sign_in_path: "/dashboard"
363
+ }
364
+ # Other keys (root_path, session_key, etc.) remain unchanged
365
+ ```
366
+
367
+ Both approaches are valid. Use direct key assignment for single changes, or use `default_scope=` for updating multiple keys at once.
368
+
369
+ #### Filtering by authenticate target type
370
+
371
+ You can restrict authentication to specific target types by setting `authenticate_for`:
372
+
373
+ ```ruby
374
+ Aikotoba.add_scope(:admin, {
375
+ authenticate_for: "Admin", # Only Admin accounts can sign in to this scope
376
+ root_path: "/admin",
377
+ sign_in_path: "/admin/sign_in",
378
+ # ...
379
+ })
380
+ ```
381
+
382
+ When `authenticate_for` is set, only accounts with matching `authenticate_target_type` will authenticate successfully in that scope.
383
+ This is useful for separating authentication between different user types (e.g., Admin vs User).
384
+
385
+ **Note:** The account type is determined by the model associated via `authenticate_target`. Set up your associations in `after_create_account_process`:
386
+
387
+ ```ruby
388
+ Rails.application.config.to_prepare do
389
+ Aikotoba::AccountsController.class_eval do
390
+ def after_create_account_process
391
+ if aikotoba_scope.admin?
392
+ admin = Admin.new(nickname: "admin_foo")
393
+ @account.authenticate_target = admin
394
+ admin.save!
395
+ else
396
+ user = User.new(nickname: "foo")
397
+ @account.authenticate_target = user
398
+ user.save!
399
+ end
400
+ @account.save!
401
+ end
402
+ end
403
+ end
193
404
  ```
194
405
 
195
406
  ## Tips
@@ -218,9 +429,17 @@ require 'aikotoba'
218
429
  Rails.application.config.to_prepare do
219
430
  Aikotoba::AccountsController.class_eval do
220
431
  def after_create_account_process
221
- profile = Profile.new(nickname: "foo")
222
- profile.save!
223
- @account.update!(authenticate_target: profile)
432
+ # You can get the scope name by `aikotoba_scope` method.
433
+ if aikotoba_scope.admin?
434
+ admin = Admin.new(nickname: "admin_foo")
435
+ @account.authenticate_target = admin
436
+ admin.save!
437
+ else
438
+ user = User.new(nickname: "foo")
439
+ @account.authenticate_target = user
440
+ user.save!
441
+ end
442
+ @account.save!
224
443
  end
225
444
  end
226
445
  end
@@ -229,8 +448,20 @@ class Profile < ApplicationRecord
229
448
  has_one :account, class_name: 'Aikotoba::Account', as: :authenticate_target
230
449
  end
231
450
 
451
+ class Admin < ApplicationRecord
452
+ has_one :account, class_name: 'Aikotoba::Account', as: :authenticate_target
453
+ end
454
+ ```
455
+
456
+ Then, you can get the associated model from `Aikotoba::Account` instance.
457
+
458
+ ```ruby
459
+
232
460
  current_account.profile #=> Profile instance
233
461
  profile.account #=> Aikotoba::Account instance
462
+
463
+ current_account.admin #=> Admin instance
464
+ admin.account #=> Aikotoba::Account instance
234
465
  ```
235
466
 
236
467
  ### Do something on before, after, failure.
@@ -257,7 +488,7 @@ Tokens can be encrypted using Active Record Encryption, introduced in Active Rec
257
488
  To use it, enable Aikotoba.encipted_token in the initializer.
258
489
 
259
490
  ```ruby
260
- Aikotoba.encypted_token = true
491
+ Aikotoba.encrypted_token = true
261
492
  ```
262
493
 
263
494
  ### How to identify the controller provided.
data/Rakefile CHANGED
@@ -3,8 +3,6 @@ require "bundler/setup"
3
3
  APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
4
  load "rails/tasks/engine.rake"
5
5
 
6
- load "rails/tasks/statistics.rake"
7
-
8
6
  require "bundler/gem_tasks"
9
7
 
10
8
  require "rake/testtask"
@@ -31,11 +31,11 @@ module Aikotoba
31
31
  end
32
32
 
33
33
  def save_with_callbacks!(account)
34
- Account::Service::Registration.call!(account: account)
34
+ account.register!
35
35
  end
36
36
 
37
37
  def after_sign_up_path
38
- aikotoba.new_session_path
38
+ aikotoba_scoped_path(:new_session_path)
39
39
  end
40
40
 
41
41
  def successed_message
@@ -3,8 +3,9 @@
3
3
  module Aikotoba
4
4
  class ApplicationController < Aikotoba.parent_controller.constantize
5
5
  include EnabledFeatureCheckable
6
+ include Scopable
6
7
 
7
- helper_method :confirmable?, :lockable?, :recoverable?, :registerable?
8
+ helper_method :confirmable?, :lockable?, :recoverable?, :registerable?, :aikotoba_scoped_path, :aikotoba_scoped_path, :aikotoba_scope
8
9
 
9
10
  def aikotoba_controller?
10
11
  true
@@ -2,9 +2,14 @@
2
2
 
3
3
  module Aikotoba
4
4
  class ConfirmsController < ApplicationController
5
- include Protection::TimingAtack
5
+ include Protection::RateLimiting
6
6
 
7
- before_action :prevent_timing_atack, only: [:update]
7
+ def self.confirmation_rate_limit_options
8
+ Aikotoba.confirmation_rate_limit_options
9
+ end
10
+ private_class_method :confirmation_rate_limit_options
11
+
12
+ rate_limit(**confirmation_rate_limit_options)
8
13
 
9
14
  def new
10
15
  @account = build_account({email: "", password: ""})
@@ -15,12 +20,11 @@ module Aikotoba
15
20
  before_send_confirmation_token_process
16
21
  send_token_account!(account)
17
22
  after_send_confirmation_token_process
18
- redirect_to success_send_confirmation_token_path, flash: {notice: success_send_confirmation_token_message}
19
23
  rescue ActiveRecord::RecordNotFound => e
20
24
  failed_send_confirmation_token_process(e)
21
- @account = build_account({email: "", password: ""})
22
- flash[:alert] = failed_send_confirmation_token_message
23
- render :new, status: :unprocessable_entity
25
+ ensure
26
+ # NOTE: Always show success message to avoid account enumeration.
27
+ redirect_to success_send_confirmation_token_path, flash: {notice: success_send_confirmation_token_message}
24
28
  end
25
29
 
26
30
  def update
@@ -46,7 +50,7 @@ module Aikotoba
46
50
  end
47
51
 
48
52
  def send_token_account!(account)
49
- Account::Service::Confirmation.create_token!(account: account, notify: true)
53
+ Account::Confirmation.create_token!(account: account, notify: true)
50
54
  end
51
55
 
52
56
  def find_by_has_token_account!(params)
@@ -56,16 +60,16 @@ module Aikotoba
56
60
  def confirm_account!(account)
57
61
  # NOTE: Confirmation is done using URL tokens, so it is done in the writing role.
58
62
  ActiveRecord::Base.connected_to(role: :writing) do
59
- Account::Service::Confirmation.confirm!(account: account)
63
+ Account::Confirmation.confirm!(account: account)
60
64
  end
61
65
  end
62
66
 
63
67
  def after_confirmed_path
64
- aikotoba.new_session_path
68
+ aikotoba_scoped_path(:new_session_path)
65
69
  end
66
70
 
67
71
  def success_send_confirmation_token_path
68
- aikotoba.new_session_path
72
+ aikotoba_scoped_path(:new_session_path)
69
73
  end
70
74
 
71
75
  def confirmed_message
@@ -76,10 +80,6 @@ module Aikotoba
76
80
  I18n.t(".aikotoba.messages.confirmation.sent")
77
81
  end
78
82
 
79
- def failed_send_confirmation_token_message
80
- I18n.t(".aikotoba.messages.confirmation.failed")
81
- end
82
-
83
83
  # NOTE: Methods to override if you want to do something before send confirm token.
84
84
  def before_send_confirmation_token_process
85
85
  end
@@ -2,9 +2,14 @@
2
2
 
3
3
  module Aikotoba
4
4
  class RecoveriesController < ApplicationController
5
- include Protection::TimingAtack
5
+ include Protection::RateLimiting
6
6
 
7
- before_action :prevent_timing_atack, only: [:edit, :update]
7
+ def self.recovery_rate_limit_options
8
+ Aikotoba.recovery_rate_limit_options
9
+ end
10
+ private_class_method :recovery_rate_limit_options
11
+
12
+ rate_limit(**recovery_rate_limit_options)
8
13
 
9
14
  def new
10
15
  @account = build_account({email: "", password: ""})
@@ -15,12 +20,11 @@ module Aikotoba
15
20
  before_send_recovery_token_process
16
21
  send_recovery_token!(account)
17
22
  after_send_recovery_token_process
18
- redirect_to success_send_recovery_token_path, flash: {notice: success_send_recovery_token_message}
19
23
  rescue ActiveRecord::RecordNotFound => e
20
24
  failed_send_recovery_token_process(e)
21
- @account = build_account({email: "", password: ""})
22
- flash[:alert] = failed_send_recovery_token_message
23
- render :new, status: :unprocessable_entity
25
+ ensure
26
+ # NOTE: Always show success message to avoid account enumeration.
27
+ redirect_to success_send_recovery_token_path, flash: {notice: success_send_recovery_token_message}
24
28
  end
25
29
 
26
30
  def edit
@@ -62,19 +66,19 @@ module Aikotoba
62
66
  end
63
67
 
64
68
  def send_recovery_token!(account)
65
- Account::Service::Recovery.create_token!(account: account, notify: true)
69
+ Account::Recovery.create_token!(account: account, notify: true)
66
70
  end
67
71
 
68
72
  def recover_account!(account, new_password)
69
- Account::Service::Recovery.recover!(account: account, new_password: new_password)
73
+ Account::Recovery.recover!(account: account, new_password: new_password)
70
74
  end
71
75
 
72
76
  def success_recovered_path
73
- aikotoba.new_session_path
77
+ aikotoba_scoped_path(:new_session_path)
74
78
  end
75
79
 
76
80
  def success_send_recovery_token_path
77
- aikotoba.new_session_path
81
+ aikotoba_scoped_path(:new_session_path)
78
82
  end
79
83
 
80
84
  def failed_message
@@ -89,10 +93,6 @@ module Aikotoba
89
93
  I18n.t(".aikotoba.messages.recovery.sent")
90
94
  end
91
95
 
92
- def failed_send_recovery_token_message
93
- I18n.t(".aikotoba.messages.recovery.sent_failed")
94
- end
95
-
96
96
  # NOTE: Methods to override if you want to do something before send recover token.
97
97
  def before_send_recovery_token_process
98
98
  end
@@ -40,15 +40,15 @@ module Aikotoba
40
40
  end
41
41
 
42
42
  def authenticate_account(params)
43
- Account.authenticate_by(attributes: params)
43
+ Account.authenticate_by(attributes: params, target_type_name: aikotoba_authenticate_target)
44
44
  end
45
45
 
46
46
  def after_sign_in_path
47
- Aikotoba.after_sign_in_path
47
+ aikotoba_scope_config[:after_sign_in_path]
48
48
  end
49
49
 
50
50
  def after_sign_out_path
51
- Aikotoba.after_sign_out_path
51
+ aikotoba_scope_config[:after_sign_out_path]
52
52
  end
53
53
 
54
54
  def successed_message
@@ -2,9 +2,14 @@
2
2
 
3
3
  module Aikotoba
4
4
  class UnlocksController < ApplicationController
5
- include Protection::TimingAtack
5
+ include Protection::RateLimiting
6
6
 
7
- before_action :prevent_timing_atack, only: [:update]
7
+ def self.unlock_rate_limit_options
8
+ Aikotoba.unlock_rate_limit_options
9
+ end
10
+ private_class_method :unlock_rate_limit_options
11
+
12
+ rate_limit(**unlock_rate_limit_options)
8
13
 
9
14
  def new
10
15
  @account = build_account({email: "", password: ""})
@@ -15,12 +20,11 @@ module Aikotoba
15
20
  before_send_unlock_token_process
16
21
  send_token_account!(account)
17
22
  after_send_unlock_token_process
18
- redirect_to success_send_unlock_token_path, flash: {notice: success_send_unlock_token_message}
19
23
  rescue ActiveRecord::RecordNotFound => e
20
24
  failed_send_unlock_token_process(e)
21
- @account = build_account({email: "", password: ""})
22
- flash[:alert] = failed_send_unlock_token_message
23
- render :new, status: :unprocessable_entity
25
+ ensure
26
+ # NOTE: Always show success message to avoid account enumeration.
27
+ redirect_to success_send_unlock_token_path, flash: {notice: success_send_unlock_token_message}
24
28
  end
25
29
 
26
30
  def update
@@ -46,7 +50,7 @@ module Aikotoba
46
50
  end
47
51
 
48
52
  def send_token_account!(account)
49
- Account::Service::Lock.create_unlock_token!(account: account, notify: true)
53
+ Account::Lock.create_unlock_token!(account: account, notify: true)
50
54
  end
51
55
 
52
56
  def find_by_has_token_account!(params)
@@ -56,16 +60,16 @@ module Aikotoba
56
60
  def unlock_account!(account)
57
61
  # NOTE: Unlocking is done using URL tokens, so it is done in the writing role.
58
62
  ActiveRecord::Base.connected_to(role: :writing) do
59
- Account::Service::Lock.unlock!(account: account)
63
+ Account::Lock.unlock!(account: account)
60
64
  end
61
65
  end
62
66
 
63
67
  def after_unlocked_path
64
- aikotoba.new_session_path
68
+ aikotoba_scoped_path(:new_session_path)
65
69
  end
66
70
 
67
71
  def success_send_unlock_token_path
68
- aikotoba.new_session_path
72
+ aikotoba_scoped_path(:new_session_path)
69
73
  end
70
74
 
71
75
  def unlocked_message
@@ -76,10 +80,6 @@ module Aikotoba
76
80
  I18n.t(".aikotoba.messages.unlocking.sent")
77
81
  end
78
82
 
79
- def failed_send_unlock_token_message
80
- I18n.t(".aikotoba.messages.unlocking.failed")
81
- end
82
-
83
83
  # NOTE: Methods to override if you want to do something before send unlock token.
84
84
  def before_send_unlock_token_process
85
85
  end