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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +334 -0
- data/Rakefile +18 -0
- data/app/controllers/aikotoba/accounts_controller.rb +61 -0
- data/app/controllers/aikotoba/application_controller.rb +13 -0
- data/app/controllers/aikotoba/confirms_controller.rb +103 -0
- data/app/controllers/aikotoba/recoveries_controller.rb +120 -0
- data/app/controllers/aikotoba/sessions_controller.rb +78 -0
- data/app/controllers/aikotoba/unlocks_controller.rb +103 -0
- data/app/controllers/concerns/aikotoba/authenticatable.rb +40 -0
- data/app/controllers/concerns/aikotoba/protection/session_fixation_attack.rb +21 -0
- data/app/controllers/concerns/aikotoba/protection/timing_atack.rb +23 -0
- data/app/mailers/aikotoba/account_mailer.rb +24 -0
- data/app/mailers/aikotoba/application_mailer.rb +5 -0
- data/app/models/aikotoba/account/confirmation_token.rb +22 -0
- data/app/models/aikotoba/account/recovery_token.rb +22 -0
- data/app/models/aikotoba/account/service/authentication.rb +65 -0
- data/app/models/aikotoba/account/service/confirmation.rb +31 -0
- data/app/models/aikotoba/account/service/lock.rb +42 -0
- data/app/models/aikotoba/account/service/recovery.rb +31 -0
- data/app/models/aikotoba/account/service/registration.rb +30 -0
- data/app/models/aikotoba/account/unlock_token.rb +22 -0
- data/app/models/aikotoba/account/value/password.rb +48 -0
- data/app/models/aikotoba/account/value/token.rb +18 -0
- data/app/models/aikotoba/account.rb +120 -0
- data/app/models/concerns/aikotoba/enabled_feature_checkable.rb +41 -0
- data/app/models/concerns/aikotoba/token_encryptable.rb +27 -0
- data/app/views/aikotoba/account_mailer/confirm.html.erb +3 -0
- data/app/views/aikotoba/account_mailer/recover.html.erb +3 -0
- data/app/views/aikotoba/account_mailer/unlock.html.erb +3 -0
- data/app/views/aikotoba/accounts/new.html.erb +19 -0
- data/app/views/aikotoba/common/_errors.html.erb +9 -0
- data/app/views/aikotoba/common/_message.html.erb +8 -0
- data/app/views/aikotoba/confirms/new.html.erb +14 -0
- data/app/views/aikotoba/recoveries/edit.html.erb +15 -0
- data/app/views/aikotoba/recoveries/new.html.erb +14 -0
- data/app/views/aikotoba/sessions/_links.html.erb +20 -0
- data/app/views/aikotoba/sessions/new.html.erb +16 -0
- data/app/views/aikotoba/unlocks/new.html.erb +21 -0
- data/app/views/layouts/aikotoba/application.html.erb +11 -0
- data/app/views/layouts/aikotoba/mailer.html.erb +13 -0
- data/app/views/layouts/aikotoba/mailer.text.erb +1 -0
- data/config/locales/en.yml +49 -0
- data/config/routes.rb +32 -0
- data/db/migrate/20211204121532_create_aikotoba_accounts.rb +50 -0
- data/lib/aikotoba/constraints/confirmable_constraint.rb +7 -0
- data/lib/aikotoba/constraints/lockable_constraint.rb +7 -0
- data/lib/aikotoba/constraints/recoverable_constraint.rb +7 -0
- data/lib/aikotoba/constraints/registerable_constraint.rb +7 -0
- data/lib/aikotoba/engine.rb +7 -0
- data/lib/aikotoba/errors.rb +7 -0
- data/lib/aikotoba/test/authentication_helper.rb +48 -0
- data/lib/aikotoba/version.rb +5 -0
- data/lib/aikotoba.rb +45 -0
- data/lib/tasks/aikotoba_tasks.rake +4 -0
- 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,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,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 @@
|
|
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,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
|
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
|