aikotoba 0.1.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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +334 -0
  4. data/Rakefile +18 -0
  5. data/app/controllers/aikotoba/accounts_controller.rb +61 -0
  6. data/app/controllers/aikotoba/application_controller.rb +13 -0
  7. data/app/controllers/aikotoba/confirms_controller.rb +103 -0
  8. data/app/controllers/aikotoba/recoveries_controller.rb +120 -0
  9. data/app/controllers/aikotoba/sessions_controller.rb +78 -0
  10. data/app/controllers/aikotoba/unlocks_controller.rb +103 -0
  11. data/app/controllers/concerns/aikotoba/authenticatable.rb +40 -0
  12. data/app/controllers/concerns/aikotoba/protection/session_fixation_attack.rb +21 -0
  13. data/app/controllers/concerns/aikotoba/protection/timing_atack.rb +23 -0
  14. data/app/mailers/aikotoba/account_mailer.rb +24 -0
  15. data/app/mailers/aikotoba/application_mailer.rb +5 -0
  16. data/app/models/aikotoba/account/confirmation_token.rb +22 -0
  17. data/app/models/aikotoba/account/recovery_token.rb +22 -0
  18. data/app/models/aikotoba/account/service/authentication.rb +65 -0
  19. data/app/models/aikotoba/account/service/confirmation.rb +31 -0
  20. data/app/models/aikotoba/account/service/lock.rb +42 -0
  21. data/app/models/aikotoba/account/service/recovery.rb +31 -0
  22. data/app/models/aikotoba/account/service/registration.rb +30 -0
  23. data/app/models/aikotoba/account/unlock_token.rb +22 -0
  24. data/app/models/aikotoba/account/value/password.rb +48 -0
  25. data/app/models/aikotoba/account/value/token.rb +18 -0
  26. data/app/models/aikotoba/account.rb +120 -0
  27. data/app/models/concerns/aikotoba/enabled_feature_checkable.rb +41 -0
  28. data/app/models/concerns/aikotoba/token_encryptable.rb +27 -0
  29. data/app/views/aikotoba/account_mailer/confirm.html.erb +3 -0
  30. data/app/views/aikotoba/account_mailer/recover.html.erb +3 -0
  31. data/app/views/aikotoba/account_mailer/unlock.html.erb +3 -0
  32. data/app/views/aikotoba/accounts/new.html.erb +19 -0
  33. data/app/views/aikotoba/common/_errors.html.erb +9 -0
  34. data/app/views/aikotoba/common/_message.html.erb +8 -0
  35. data/app/views/aikotoba/confirms/new.html.erb +14 -0
  36. data/app/views/aikotoba/recoveries/edit.html.erb +15 -0
  37. data/app/views/aikotoba/recoveries/new.html.erb +14 -0
  38. data/app/views/aikotoba/sessions/_links.html.erb +20 -0
  39. data/app/views/aikotoba/sessions/new.html.erb +16 -0
  40. data/app/views/aikotoba/unlocks/new.html.erb +21 -0
  41. data/app/views/layouts/aikotoba/application.html.erb +11 -0
  42. data/app/views/layouts/aikotoba/mailer.html.erb +13 -0
  43. data/app/views/layouts/aikotoba/mailer.text.erb +1 -0
  44. data/config/locales/en.yml +49 -0
  45. data/config/routes.rb +32 -0
  46. data/db/migrate/20211204121532_create_aikotoba_accounts.rb +50 -0
  47. data/lib/aikotoba/constraints/confirmable_constraint.rb +7 -0
  48. data/lib/aikotoba/constraints/lockable_constraint.rb +7 -0
  49. data/lib/aikotoba/constraints/recoverable_constraint.rb +7 -0
  50. data/lib/aikotoba/constraints/registerable_constraint.rb +7 -0
  51. data/lib/aikotoba/engine.rb +7 -0
  52. data/lib/aikotoba/errors.rb +7 -0
  53. data/lib/aikotoba/test/authentication_helper.rb +48 -0
  54. data/lib/aikotoba/version.rb +5 -0
  55. data/lib/aikotoba.rb +45 -0
  56. data/lib/tasks/aikotoba_tasks.rake +4 -0
  57. metadata +128 -0
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikotoba
4
+ class Account < ApplicationRecord
5
+ include EnabledFeatureCheckable
6
+ # NOTE: (RFC5321) Path: The maximum total length of a reverse-path or forward-path is 256 octets.
7
+ # https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3.1.3
8
+ EMAIL_MAXIMUM_LENGTH = 256
9
+ EMAIL_REGEXP = Aikotoba.email_format
10
+
11
+ belongs_to :authenticate_target, polymorphic: true, optional: true
12
+
13
+ attribute :max_failed_attempts, :integer, default: -> { Aikotoba.max_failed_attempts }
14
+
15
+ validates :email, presence: true, uniqueness: true, format: EMAIL_REGEXP, length: {maximum: EMAIL_MAXIMUM_LENGTH}
16
+ validates :password, presence: true, length: {in: Value::Password::LENGTH_RENGE}, on: [:create, :recover]
17
+ validates :password_digest, presence: true
18
+ validates :confirmed, inclusion: [true, false]
19
+ validates :failed_attempts, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
20
+ validates :max_failed_attempts, numericality: {only_integer: true, greater_than: 0}
21
+ validates :locked, inclusion: [true, false]
22
+
23
+ after_initialize do
24
+ if authenticate_target
25
+ target_type_name = authenticate_target_type.gsub("::", "").underscore
26
+ define_singleton_method(target_type_name) { authenticate_target }
27
+ end
28
+ end
29
+
30
+ attr_reader :password
31
+
32
+ def password=(value)
33
+ new_password = Value::Password.new(value: value)
34
+ @password = new_password.value
35
+ assign_attributes(password_digest: new_password.digest)
36
+ end
37
+
38
+ concerning :Authenticatable do
39
+ included do
40
+ scope :authenticatable, -> {
41
+ result = all
42
+ result = result.confirmed if confirmable?
43
+ result = result.unlocked if lockable?
44
+ result
45
+ }
46
+ end
47
+
48
+ class_methods do
49
+ def authenticate_by(attributes:)
50
+ email, password = attributes.values_at(:email, :password)
51
+ Service::Authentication.call!(email: email, password: password)
52
+ end
53
+ end
54
+
55
+ def password_match?(password)
56
+ Value::Password.new(value: password).match?(digest: password_digest)
57
+ end
58
+
59
+ def authentication_failed!
60
+ increment!(:failed_attempts)
61
+ end
62
+
63
+ def authentication_success!
64
+ update!(failed_attempts: 0)
65
+ end
66
+ end
67
+
68
+ concerning :Registrable do
69
+ class_methods do
70
+ def build_by(attributes:)
71
+ email, password = attributes.values_at(:email, :password)
72
+ new(email: email).tap { |account| account.password = password }
73
+ end
74
+ end
75
+ end
76
+
77
+ concerning :Confirmable do
78
+ included do
79
+ has_one :confirmation_token, dependent: :destroy, foreign_key: "aikotoba_account_id"
80
+ scope :confirmed, -> { where(confirmed: true) }
81
+ scope :unconfirmed, -> { where(confirmed: false) }
82
+ end
83
+
84
+ def confirm!
85
+ update!(confirmed: true)
86
+ end
87
+ end
88
+
89
+ concerning :Lockable do
90
+ included do
91
+ has_one :unlock_token, dependent: :destroy, foreign_key: "aikotoba_account_id"
92
+ scope :locked, -> { where(locked: true) }
93
+ scope :unlocked, -> { where(locked: false) }
94
+ end
95
+
96
+ def should_lock?
97
+ failed_attempts > max_failed_attempts
98
+ end
99
+
100
+ def lock!
101
+ update!(locked: true)
102
+ end
103
+
104
+ def unlock!
105
+ update!(locked: false, failed_attempts: 0)
106
+ end
107
+ end
108
+
109
+ concerning :Recoverable do
110
+ included do
111
+ has_one :recovery_token, dependent: :destroy, foreign_key: "aikotoba_account_id"
112
+ end
113
+
114
+ def recover!(new_password:)
115
+ self.password = new_password
116
+ save!(context: :recover)
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikotoba
4
+ module EnabledFeatureCheckable
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def registerable?
9
+ Aikotoba.registerable
10
+ end
11
+
12
+ def lockable?
13
+ Aikotoba.lockable
14
+ end
15
+
16
+ def confirmable?
17
+ Aikotoba.confirmable
18
+ end
19
+
20
+ def recoverable?
21
+ Aikotoba.recoverable
22
+ end
23
+ end
24
+
25
+ def registerable?
26
+ self.class.registerable?
27
+ end
28
+
29
+ def lockable?
30
+ self.class.lockable?
31
+ end
32
+
33
+ def confirmable?
34
+ self.class.confirmable?
35
+ end
36
+
37
+ def recoverable?
38
+ self.class.recoverable?
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikotoba
4
+ module TokenEncryptable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ if enabled_aikotoba_enctypted_token?
9
+ if available_active_record_encryption?
10
+ encrypts :token, deterministic: true
11
+ else
12
+ raise Errors::NotAvailableException, "You need to be able to encrypt the token using Active Record Encryption."
13
+ end
14
+ end
15
+ end
16
+
17
+ module ClassMethods
18
+ def enabled_aikotoba_enctypted_token?
19
+ Aikotoba.encypted_token
20
+ end
21
+
22
+ def available_active_record_encryption?
23
+ ActiveRecord::VERSION::MAJOR >= 7
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ Confirm url: <%= @confirm_url %>
2
+ <br>
3
+ The url expires at <%= I18n.l(@token.expired_at, format: :long) %>
@@ -0,0 +1,3 @@
1
+ Password reset url: <%= @recover_url %>
2
+ <br>
3
+ The url expires at <%= I18n.l(@token.expired_at, format: :long) %>
@@ -0,0 +1,3 @@
1
+ Unlock url: <%= @unlock_url %>
2
+ <br>
3
+ The url expires at <%= I18n.l(@token.expired_at, format: :long) %>
@@ -0,0 +1,19 @@
1
+ <div class="aikotoba-accounts-new">
2
+ <%= render "aikotoba/common/message" %>
3
+ <h1><%= I18n.t(".aikotoba.accounts.new") %></h1>
4
+ <%= form_with model: @account, method: :post, url: aikotoba.create_account_path do |f| %>
5
+ <%= render "aikotoba/common/errors", resource: @account %>
6
+ <p>
7
+ <%= f.label :email %>
8
+ <%= f.email_field :email, required: true %>
9
+ </p>
10
+ <p>
11
+ <%= f.label :password %>
12
+ <%= f.password_field :password, required: true %>
13
+ </p>
14
+ <%= f.submit I18n.t(".aikotoba.accounts.new") %>
15
+ <% end %>
16
+ <p>
17
+ <%= link_to I18n.t(".aikotoba.sessions.new"), aikotoba.new_session_path %>
18
+ </p>
19
+ </div>
@@ -0,0 +1,9 @@
1
+ <% if resource.errors.any? %>
2
+ <div class="errors">
3
+ <ul>
4
+ <% resource.errors.full_messages.each do |message| %>
5
+ <li><%= message %></li>
6
+ <% end %>
7
+ </ul>
8
+ <div>
9
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <div class="message">
2
+ <% if notice %>
3
+ <p><%= notice %></p>
4
+ <% end %>
5
+ <% if alert %>
6
+ <p><%= alert %></p>
7
+ <% end %>
8
+ </div>
@@ -0,0 +1,14 @@
1
+ <div class="aikotoba-confirmation-new">
2
+ <%= render "aikotoba/common/message" %>
3
+ <h1><%= I18n.t(".aikotoba.confirms.new") %></h1>
4
+ <%= form_with model: @account, method: :post, url: aikotoba.create_confirmation_token_path do |f| %>
5
+ <p>
6
+ <%= f.label :email %>
7
+ <%= f.email_field :email, required: true %>
8
+ </p>
9
+ <%= f.submit I18n.t(".aikotoba.confirms.new") %>
10
+ <% end %>
11
+ <p>
12
+ <%= link_to I18n.t(".aikotoba.sessions.new"), aikotoba.new_session_path %>
13
+ </p>
14
+ </div>
@@ -0,0 +1,15 @@
1
+ <div class="aikotoba-recoveries-edit">
2
+ <%= render "aikotoba/common/message" %>
3
+ <h1><%= I18n.t(".aikotoba.recoveries.edit") %></h1>
4
+ <%= form_with model: @account, method: :patch, url: aikotoba.update_account_password_path(token: @account.recovery_token.token) do |f| %>
5
+ <%= render "aikotoba/common/errors", resource: @account %>
6
+ <p>
7
+ <%= f.label :password %>
8
+ <%= f.password_field :password, required: true %>
9
+ </p>
10
+ <%= f.submit I18n.t(".aikotoba.recoveries.edit") %>
11
+ <% end %>
12
+ <p>
13
+ <%= link_to I18n.t(".aikotoba.sessions.new"), aikotoba.new_session_path %>
14
+ </p>
15
+ </div>
@@ -0,0 +1,14 @@
1
+ <div class="aikotoba-recoveries-new">
2
+ <%= render "aikotoba/common/message" %>
3
+ <h1><%= I18n.t(".aikotoba.recoveries.new") %></h1>
4
+ <%= form_with model: @account, method: :post, url: aikotoba.create_recovery_token_path do |f| %>
5
+ <p>
6
+ <%= f.label :email %>
7
+ <%= f.email_field :email, required: true %>
8
+ </p>
9
+ <%= f.submit I18n.t(".aikotoba.recoveries.new") %>
10
+ <% end %>
11
+ <p>
12
+ <%= link_to I18n.t(".aikotoba.sessions.new"), aikotoba.new_session_path %>
13
+ </p>
14
+ </div>
@@ -0,0 +1,20 @@
1
+ <% if registerable? %>
2
+ <p>
3
+ <%= link_to I18n.t(".aikotoba.accounts.new"), aikotoba.new_account_path %>
4
+ </p>
5
+ <% end %>
6
+ <% if confirmable? %>
7
+ <p>
8
+ <%= link_to I18n.t(".aikotoba.confirms.new"), aikotoba.new_confirmation_token_path %>
9
+ </p>
10
+ <% end %>
11
+ <% if lockable? %>
12
+ <p>
13
+ <%= link_to I18n.t(".aikotoba.unlocks.new"), aikotoba.new_unlock_token_path %>
14
+ </p>
15
+ <% end %>
16
+ <% if recoverable? %>
17
+ <p>
18
+ <%= link_to I18n.t(".aikotoba.recoveries.new"), aikotoba.new_recovery_token_path %>
19
+ </p>
20
+ <% end %>
@@ -0,0 +1,16 @@
1
+ <div class="aikotoba-sessions-new">
2
+ <%= render "aikotoba/common/message" %>
3
+ <h1><%= I18n.t(".aikotoba.sessions.new") %></h1>
4
+ <%= form_with model: @account, method: :post, url: aikotoba.new_session_path do |f| %>
5
+ <p>
6
+ <%= f.label :email %>
7
+ <%= f.email_field :email, required: true %>
8
+ </p>
9
+ <p>
10
+ <%= f.label :password %>
11
+ <%= f.password_field :password, required: true %>
12
+ </p>
13
+ <%= f.submit I18n.t(".aikotoba.sessions.new") %>
14
+ <% end %>
15
+ <%= render 'links' %>
16
+ </div>
@@ -0,0 +1,21 @@
1
+ <div class="aikotoba-unlock-new">
2
+ <div class="message">
3
+ <% if notice %>
4
+ <p><%= notice %></p>
5
+ <% end %>
6
+ <% if alert %>
7
+ <p><%= alert %></p>
8
+ <% end %>
9
+ </div>
10
+ <h1><%= I18n.t(".aikotoba.unlocks.new") %></h1>
11
+ <%= form_with model: @account, method: :post, url: aikotoba.create_unlock_token_path do |f| %>
12
+ <p>
13
+ <%= f.label :email %>
14
+ <%= f.email_field :email, required: true %>
15
+ </p>
16
+ <%= f.submit I18n.t(".aikotoba.unlocks.new") %>
17
+ <% end %>
18
+ <p>
19
+ <%= link_to I18n.t(".aikotoba.sessions.new"), aikotoba.new_session_path %>
20
+ </p>
21
+ </div>
@@ -0,0 +1,11 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Aikotoba</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+ </head>
8
+ <body>
9
+ <%= yield %>
10
+ </body>
11
+ </html>
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
5
+ <style>
6
+ /* Email styles need to be inline */
7
+ </style>
8
+ </head>
9
+
10
+ <body>
11
+ <%= yield %>
12
+ </body>
13
+ </html>
@@ -0,0 +1 @@
1
+ <%= yield %>
@@ -0,0 +1,49 @@
1
+ en:
2
+ activerecord:
3
+ attributes:
4
+ aikotoba/account:
5
+ email: Email
6
+ password: Password
7
+ models:
8
+ aikotoba/account: Account
9
+ aikotoba:
10
+ sessions:
11
+ new: Sign in
12
+ accounts:
13
+ new: Sign up
14
+ confirms:
15
+ new: Send confirm token
16
+ unlocks:
17
+ new: Send unlock token
18
+ recoveries:
19
+ new: Send password reset token
20
+ edit: Password reset
21
+ mailers:
22
+ confirm:
23
+ subject: "Confirmation instructions"
24
+ unlock:
25
+ subject: "Unlock instructions"
26
+ recover:
27
+ subject: "Password reset instructions"
28
+
29
+ messages:
30
+ authentication:
31
+ success: Signed in successfully.
32
+ failed: Oops. Signed in failed.
33
+ sign_out: Signed out.
34
+ registration:
35
+ success: Signed up successfully.
36
+ failed: Oops. Signed up failed.
37
+ confirmation:
38
+ sent: Confirm url has been sent to your email address.
39
+ success: Confirmed you email successfully.
40
+ failed: Oops. Confirm url sent failed.
41
+ unlocking:
42
+ sent: Unlock url has been sent to your email address.
43
+ success: Unlocked you account successfully.
44
+ failed: Oops. Unlock url sent failed.
45
+ recovery:
46
+ sent: Password reset url has been sent to your email address.
47
+ sent_failed: Oops. Password reset url sent failed.
48
+ success: Password reset you account successfully.
49
+ failed: Oops. Password reset failed.
data/config/routes.rb ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "aikotoba/constraints/registerable_constraint"
4
+ require "aikotoba/constraints/confirmable_constraint"
5
+ require "aikotoba/constraints/lockable_constraint"
6
+ require "aikotoba/constraints/recoverable_constraint"
7
+
8
+ Aikotoba::Engine.routes.draw do
9
+ get(Aikotoba.sign_in_path, to: "sessions#new", as: :new_session)
10
+ post(Aikotoba.sign_in_path, to: "sessions#create", as: :create_session)
11
+ delete(Aikotoba.sign_out_path, to: "sessions#destroy", as: :destroy_session)
12
+ constraints(Aikotoba::RegisterableConstraint) do
13
+ get(Aikotoba.sign_up_path, to: "accounts#new", as: :new_account)
14
+ post(Aikotoba.sign_up_path, to: "accounts#create", as: :create_account)
15
+ end
16
+ constraints(Aikotoba::ConfirmableConstraint) do
17
+ get(Aikotoba.confirm_path, to: "confirms#new", as: :new_confirmation_token)
18
+ post(Aikotoba.confirm_path, to: "confirms#create", as: :create_confirmation_token)
19
+ get(File.join(Aikotoba.confirm_path, ":token"), to: "confirms#update", as: :confirm_account)
20
+ end
21
+ constraints(Aikotoba::LockableConstraint) do
22
+ get(Aikotoba.unlock_path, to: "unlocks#new", as: :new_unlock_token)
23
+ post(Aikotoba.unlock_path, to: "unlocks#create", as: :create_unlock_token)
24
+ get(File.join(Aikotoba.unlock_path, ":token"), to: "unlocks#update", as: :unlock_account)
25
+ end
26
+ constraints(Aikotoba::RecoverableConstraint) do
27
+ get(Aikotoba.recover_path, to: "recoveries#new", as: :new_recovery_token)
28
+ post(Aikotoba.recover_path, to: "recoveries#create", as: :create_recovery_token)
29
+ get(File.join(Aikotoba.recover_path, ":token"), to: "recoveries#edit", as: :edit_account_password)
30
+ patch(File.join(Aikotoba.recover_path, ":token"), to: "recoveries#update", as: :update_account_password)
31
+ end
32
+ end
@@ -0,0 +1,50 @@
1
+ class CreateAikotobaAccounts < ActiveRecord::Migration[6.1]
2
+ def change
3
+ create_table :aikotoba_accounts do |t|
4
+ t.belongs_to :authenticate_target, polymorphic: true, index: {unique: true}
5
+ t.string :email, null: false, index: {unique: true}
6
+ t.string :password_digest, null: false
7
+ t.boolean :confirmed, null: false, default: false
8
+ t.integer :failed_attempts, null: false, default: 0
9
+ t.boolean :locked, null: false, default: false
10
+
11
+ t.timestamps
12
+ end
13
+
14
+ create_table :aikotoba_account_confirmation_tokens do |t|
15
+ t.belongs_to(
16
+ :aikotoba_account,
17
+ foreign_key: true, null: false,
18
+ index: {unique: true, name: "index_account_confirmation_tokens_on_account_id"}
19
+ )
20
+ t.string :token, null: false, index: {unique: true}
21
+ t.datetime :expired_at, null: false
22
+
23
+ t.timestamps
24
+ end
25
+
26
+ create_table :aikotoba_account_unlock_tokens do |t|
27
+ t.belongs_to(
28
+ :aikotoba_account,
29
+ null: false, foreign_key: true,
30
+ index: {unique: true, name: "index_account_unlock_tokens_on_account_id"}
31
+ )
32
+ t.string :token, null: false, index: {unique: true}
33
+ t.datetime :expired_at, null: false
34
+
35
+ t.timestamps
36
+ end
37
+
38
+ create_table :aikotoba_account_recovery_tokens do |t|
39
+ t.belongs_to(
40
+ :aikotoba_account,
41
+ null: false, foreign_key: true,
42
+ index: {unique: true, name: "index_account_recovery_tokens_on_account_id"}
43
+ )
44
+ t.string :token, null: false, index: {unique: true}
45
+ t.datetime :expired_at, null: false
46
+
47
+ t.timestamps
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Aikotoba::ConfirmableConstraint
4
+ def self.matches?(_request)
5
+ Aikotoba.confirmable
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Aikotoba::LockableConstraint
4
+ def self.matches?(_request)
5
+ Aikotoba.lockable
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Aikotoba::RecoverableConstraint
4
+ def self.matches?(_request)
5
+ Aikotoba.recoverable
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Aikotoba::RegisterableConstraint
4
+ def self.matches?(_request)
5
+ Aikotoba.registerable
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikotoba
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Aikotoba
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Aikotoba
2
+ module Errors
3
+ class Base < StandardError; end
4
+
5
+ class NotAvailableException < Base; end
6
+ end
7
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikotoba
4
+ module Test
5
+ module AuthenticationHelper
6
+ module Request
7
+ def aikotoba_sign_out
8
+ delete aikotoba.destroy_session_path
9
+ follow_redirect!
10
+ end
11
+
12
+ def aikotoba_sign_in(account)
13
+ post aikotoba.new_session_path, params: {account: {email: account.email, password: account.password}}
14
+ follow_redirect!
15
+ end
16
+ end
17
+
18
+ module System
19
+ def aikotoba_sign_out
20
+ if page.driver.is_a?(Capybara::RackTest::Driver)
21
+ disable_forgery_protection { page.driver.send(:delete, aikotoba.destroy_session_path) }
22
+ else
23
+ raise NotImplementedError, "Sorry. Only RackTest::Driver is supported as a test helper for Aikotoba's authentication."
24
+ end
25
+ end
26
+
27
+ def aikotoba_sign_in(account)
28
+ if page.driver.is_a?(Capybara::RackTest::Driver)
29
+ disable_forgery_protection do
30
+ page.driver.send(:post, aikotoba.new_session_path, account: {email: account.email, password: account.password})
31
+ end
32
+ else
33
+ raise NotImplementedError, "Sorry. Only RackTest::Driver is supported as a test helper for Aikotoba's authentication."
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def disable_forgery_protection
40
+ csrf_protection = ActionController::Base.allow_forgery_protection
41
+ ActionController::Base.allow_forgery_protection = false
42
+ yield
43
+ ActionController::Base.allow_forgery_protection = csrf_protection
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikotoba
4
+ VERSION = "0.1.0"
5
+ end
data/lib/aikotoba.rb ADDED
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "aikotoba/version"
4
+ require "aikotoba/engine"
5
+ require "aikotoba/errors"
6
+
7
+ module Aikotoba
8
+ mattr_accessor(:parent_controller) { "ApplicationController" }
9
+ mattr_accessor(:parent_mailer) { "ActionMailer::Base" }
10
+ mattr_accessor(:mailer_sender) { "from@example.com" }
11
+ mattr_accessor(:email_format) { /\A[^\s]+@[^\s]+\z/ }
12
+ mattr_accessor(:password_pepper) { "aikotoba-default-pepper" }
13
+ mattr_accessor(:password_length_range) { 8..100 }
14
+ mattr_accessor(:session_key) { "aikotoba-account-id" }
15
+ mattr_accessor(:sign_in_path) { "/sign_in" }
16
+ mattr_accessor(:sign_out_path) { "/sign_out" }
17
+ mattr_accessor(:after_sign_in_path) { "/" }
18
+ mattr_accessor(:after_sign_out_path) { "/sign_in" }
19
+
20
+ # for encrypt token
21
+ mattr_accessor(:encypted_token) { false }
22
+
23
+ # for registerable
24
+ mattr_accessor(:registerable) { true }
25
+ mattr_accessor(:sign_up_path) { "/sign_up" }
26
+
27
+ # for confirmable
28
+ mattr_accessor(:confirmable) { false }
29
+ mattr_accessor(:confirm_path) { "/confirm" }
30
+ mattr_accessor(:confirmation_token_expiry) { 1.day }
31
+
32
+ # for lockable
33
+ mattr_accessor(:lockable) { false }
34
+ mattr_accessor(:unlock_path) { "/unlock" }
35
+ mattr_accessor(:max_failed_attempts) { 10 }
36
+ mattr_accessor(:unlock_token_expiry) { 1.day }
37
+
38
+ # for recoverable
39
+ mattr_accessor(:recoverable) { false }
40
+ mattr_accessor(:recover_path) { "/recover" }
41
+ mattr_accessor(:recovery_token_expiry) { 4.hours }
42
+
43
+ # for security
44
+ mattr_accessor(:prevent_timing_atack) { true }
45
+ end