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