aikotoba 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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