avocado 0.4.0 → 0.5.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +4 -0
  4. data/app/controllers/avocado/affirmations_controller.rb +51 -0
  5. data/app/controllers/avocado/base_controller.rb +10 -0
  6. data/app/controllers/avocado/emails_controller.rb +48 -0
  7. data/app/controllers/avocado/events_controller.rb +9 -0
  8. data/app/controllers/avocado/passwords_controller.rb +34 -0
  9. data/app/views/avocado/affirmations/new.html.erb +14 -0
  10. data/app/views/avocado/emails/edit.html.erb +20 -0
  11. data/app/views/avocado/events/_event.html.erb +6 -0
  12. data/app/views/avocado/events/index.html.erb +17 -0
  13. data/app/views/avocado/mailer/email_affirmation.text.erb +3 -1
  14. data/app/views/avocado/mailer/email_verification.text.erb +1 -1
  15. data/app/views/avocado/mailer/password_reset.text.erb +1 -1
  16. data/app/views/avocado/passwords/edit.html.erb +16 -0
  17. data/app/views/avocado/recoveries/edit.html.erb +2 -2
  18. data/app/views/avocado/recoveries/new.html.erb +2 -2
  19. data/app/views/avocado/registrations/new.html.erb +1 -1
  20. data/app/views/avocado/sessions/new.html.erb +1 -1
  21. data/config/routes.rb +4 -0
  22. data/lib/avocado/current.rb +10 -8
  23. data/lib/avocado/event.rb +32 -0
  24. data/lib/avocado/session.rb +3 -0
  25. data/lib/avocado/session_callbacks.rb +34 -0
  26. data/lib/avocado/user.rb +10 -3
  27. data/lib/avocado/user_callbacks.rb +45 -0
  28. data/lib/avocado/user_validations.rb +22 -0
  29. data/lib/avocado/version.rb +1 -1
  30. data/lib/avocado.rb +4 -2
  31. metadata +15 -4
  32. data/lib/avocado/user_email.rb +0 -13
  33. data/lib/avocado/user_password.rb +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e265345c4e35e1dd2caed0c61da1298f12767aac67bf9e87b92433d47866375b
4
- data.tar.gz: 1253d00e053907caa9d78cd7f8667a695aae55853fbb6db94b45bbcd1845da10
3
+ metadata.gz: a530ebf9605c2bb861d2da09da546513e68c4f647fc97c80686e07b32985cabe
4
+ data.tar.gz: 3be481be5c31ce2ee3bb1ea1a2d796bb7f8cbe850f7ccf2de6f296384a656bb2
5
5
  SHA512:
6
- metadata.gz: 2fb300cf06c7c67fc9ecfea196bd23b770482f2b765d9438fb2ee24f193942565db6d470a75bce99bb1b016b94f355ab9e4b6f6a5a19bb0277d79c7bb06c247d
7
- data.tar.gz: 9437dfe9000245f75089c3705a3bf4527705ceccab38a6760381d57dd13fda74881eb14db25b8ca33f26aee67fa6fd30ce42632a24541bcb1c672e6ea9eb80b5
6
+ metadata.gz: cf3320eb985cc65c05c2da2d007697b4b9016d9a85f7ebebbd0735d744d7f8ca77096d86182c6761b63ea6f51f4b942a3c2dd363bb102ccc4ed53da6f5702c4f
7
+ data.tar.gz: f0b021b9e0f3433f2034e5cccdc751a446b50bc870474376e1a39b84e973bbabfaef95b59d96f60986bbddc7695afe88b860bd2d1568f991a17b3b654d7f3995
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.0] - 2023-07-21
4
+
5
+ - Add controller for "passwordless" email-link sign-in
6
+ - Add event class to log user auth events
7
+ - Add user-facing email and password edit pages
8
+ - Add various event logging callbacks
9
+ - Sign out all non current sessions when password changes
10
+
3
11
  ## [0.4.0] - 2023-07-19
4
12
 
5
13
  - Convert the `Avocado::Mailer` module into a class
data/README.md CHANGED
@@ -29,6 +29,10 @@ class Session < ApplicationRecord
29
29
  include Avocado::Session
30
30
  end
31
31
 
32
+ class Event < ApplicationRecord
33
+ include Avocado::Event
34
+ end
35
+
32
36
  class ApplicationController < ActionController::Base
33
37
  include Avocado::Authentication
34
38
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avocado
4
+ class AffirmationsController < BaseController
5
+ skip_before_action :authenticate
6
+
7
+ before_action :set_user, only: :show
8
+ before_action :verify_user, only: :create
9
+
10
+ def new
11
+ end
12
+
13
+ def show
14
+ sign_in(@user)
15
+ redirect_to(root_path, notice: "Signed in successfully")
16
+ end
17
+
18
+ def create
19
+ send_affirmation_email
20
+ redirect_to new_session_path, notice: "Check your email for sign in instructions"
21
+ end
22
+
23
+ private
24
+
25
+ def set_user
26
+ @user = user_from_signed_affirmation_token
27
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
28
+ redirect_to new_affirmation_path, alert: "That sign in link is invalid"
29
+ end
30
+
31
+ def user_from_signed_affirmation_token
32
+ ::User.find_by_token_for!(:email_affirmation, params[:id])
33
+ end
34
+
35
+ def verify_user
36
+ unless user_from_params_email
37
+ redirect_to new_affirmation_path, alert: "You can't sign in until you verify your email"
38
+ end
39
+ end
40
+
41
+ def send_affirmation_email
42
+ mailer_for(user_from_params_email)
43
+ .email_affirmation
44
+ .deliver_later
45
+ end
46
+
47
+ def user_from_params_email
48
+ ::User.verified.find_by(email: params[:email])
49
+ end
50
+ end
51
+ end
@@ -4,6 +4,16 @@ module Avocado
4
4
  class BaseController < ApplicationController
5
5
  private
6
6
 
7
+ def verify_password_challenge
8
+ unless current_user.authenticate(params_password_challenge)
9
+ redirect_back alert: "Password challenge failed.", fallback_location: root_path
10
+ end
11
+ end
12
+
13
+ def params_password_challenge
14
+ params.dig(:user, :password_challenge)
15
+ end
16
+
7
17
  def mailer_for(user)
8
18
  Avocado::Mailer.with(user: user)
9
19
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avocado
4
+ class EmailsController < BaseController
5
+ PERMITTED_PARAMS = [:email]
6
+
7
+ before_action :set_user
8
+ before_action :verify_password_challenge, only: :update
9
+
10
+ def edit
11
+ end
12
+
13
+ def update
14
+ if @user.update(user_params)
15
+ process_email_update
16
+ else
17
+ render :edit, status: :unprocessable_entity
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def set_user
24
+ @user = current_user
25
+ end
26
+
27
+ def user_params
28
+ params
29
+ .require(:user)
30
+ .permit(PERMITTED_PARAMS)
31
+ end
32
+
33
+ def process_email_update
34
+ if @user.email_previously_changed?
35
+ resend_email_verification
36
+ redirect_to root_path, notice: "Your email has been changed"
37
+ else
38
+ redirect_to root_path
39
+ end
40
+ end
41
+
42
+ def resend_email_verification
43
+ mailer_for(@user)
44
+ .email_verification
45
+ .deliver_later
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avocado
4
+ class EventsController < BaseController
5
+ def index
6
+ @events = current_user.events.newest_first
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avocado
4
+ class PasswordsController < BaseController
5
+ PERMITTED_PARAMS = [:password, :password_confirmation, :password_challenge]
6
+
7
+ before_action :set_user
8
+ before_action :verify_password_challenge, only: :update
9
+
10
+ def edit
11
+ end
12
+
13
+ def update
14
+ if @user.update(user_params)
15
+ redirect_to root_path, notice: "Your password has been changed"
16
+ else
17
+ render :edit, status: :unprocessable_entity
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def set_user
24
+ @user = current_user
25
+ end
26
+
27
+ def user_params
28
+ params
29
+ .require(:user)
30
+ .permit(PERMITTED_PARAMS)
31
+ .with_defaults(password_challenge: "")
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,14 @@
1
+ <h2>
2
+ Sign in without password
3
+ </h2>
4
+
5
+ <p>
6
+ <%= link_to "Reset your password", new_recovery_path %>
7
+ </p>
8
+
9
+ <%= form_with url: affirmations_path do |form| %>
10
+ <%= form.label :email %>
11
+ <%= form.email_field :email, autofocus: true, autocomplete: "email", required: true %>
12
+
13
+ <%= form.button "Submit", name: nil %>
14
+ <% end -%>
@@ -0,0 +1,20 @@
1
+ <h2>
2
+ Change your email address
3
+ </h2>
4
+
5
+ <% if current_user.verified? %>
6
+ <p>Your email address is verified.</p>
7
+ <% else %>
8
+ <p>Your email address is not verified. Check your email and follow the instructions to confirm it's your email address.</p>
9
+ <p><%= button_to "Re-send verification email", verifications_path %></p>
10
+ <% end %>
11
+
12
+ <%= form_with model: @user, url: email_path, method: :patch do |form| %>
13
+ <%= form.label :email %>
14
+ <%= form.email_field :email, autocomplete: "email", required: true %>
15
+
16
+ <%= form.label :password_challenge, "Current password" %>
17
+ <%= form.password_field :password_challenge, autocomplete: "current-password", required: true %>
18
+
19
+ <%= form.button "Submit", name: nil %>
20
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <tr id="<%= dom_id event %>">
2
+ <td><%= event.action %></td>
3
+ <td><%= event.created_at %></td>
4
+ <td><%= truncate event.user_agent %></td>
5
+ <td><%= event.ip_address %></td>
6
+ </tr>
@@ -0,0 +1,17 @@
1
+ <h2>
2
+ Events
3
+ </h2>
4
+
5
+ <table id="events">
6
+ <thead>
7
+ <tr>
8
+ <th scope="col">Action</th>
9
+ <th scope="col">Created</th>
10
+ <th scope="col">User Agent</th>
11
+ <th scope="col">IP Address</th>
12
+ </tr>
13
+ </thead>
14
+ <tbody>
15
+ <%= render @events %>
16
+ </tbody>
17
+ </table>
@@ -1 +1,3 @@
1
- Email affirmation email
1
+ Sign in by following this link:
2
+
3
+ <%= affirmation_url(id: @signed_id) %>
@@ -1,3 +1,3 @@
1
- Email verification email
1
+ Verify your email by following this link:
2
2
 
3
3
  <%= verification_url(id: @signed_id) %>
@@ -1,3 +1,3 @@
1
- Password reset email
1
+ Reset your password by following this link:
2
2
 
3
3
  <%= edit_recovery_url(id: @signed_id) %>
@@ -0,0 +1,16 @@
1
+ <h2>
2
+ Change your password
3
+ </h2>
4
+
5
+ <%= form_with model: @user, url: password_path, method: :patch do |form| %>
6
+ <%= form.label :password_challenge, "Current password" %>
7
+ <%= form.password_field :password_challenge, autocomplete: "current-password", required: true %>
8
+
9
+ <%= form.label :password %>
10
+ <%= form.password_field :password, autocomplete: "new-password", required: true %>
11
+
12
+ <%= form.label :password_confirmation %>
13
+ <%= form.password_field :password_confirmation, autocomplete: "new-password", required: true %>
14
+
15
+ <%= form.button "Submit", name: nil %>
16
+ <% end %>
@@ -1,5 +1,5 @@
1
1
  <h2>
2
- Reset your password
2
+ Change your password
3
3
  </h2>
4
4
 
5
5
  <p>
@@ -13,5 +13,5 @@
13
13
  <%= form.label :password_confirmation %>
14
14
  <%= form.password_field :password_confirmation, autocomplete: "new-password", required: true %>
15
15
 
16
- <%= form.button "Update password", name: nil %>
16
+ <%= form.button "Submit", name: nil %>
17
17
  <% end %>
@@ -1,5 +1,5 @@
1
1
  <h2>
2
- Recover your password
2
+ Reset your password
3
3
  </h2>
4
4
 
5
5
  <p>
@@ -10,5 +10,5 @@
10
10
  <%= form.label :email %>
11
11
  <%= form.email_field :email, autofocus: true, autocomplete: "email", required: true %>
12
12
 
13
- <%= form.button "Recover", name: nil %>
13
+ <%= form.button "Submit", name: nil %>
14
14
  <% end -%>
@@ -19,5 +19,5 @@
19
19
  <%= form.label :password_confirmation %>
20
20
  <%= form.password_field :password_confirmation, autocomplete: "new-password", required: true %>
21
21
  </div>
22
- <%= form.button "Sign up", name: nil %>
22
+ <%= form.button "Submit", name: nil %>
23
23
  <% end %>
@@ -11,5 +11,5 @@
11
11
  <%= form.email_field :email, autofocus: true, autocomplete: "email", required: true %>
12
12
  <%= form.label :password %>
13
13
  <%= form.password_field :password, autocomplete: "current-password", required: true %>
14
- <%= form.button "Sign in", name: nil %>
14
+ <%= form.button "Submit", name: nil %>
15
15
  <% end -%>
data/config/routes.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  Rails.application.routes.draw do
2
2
  scope module: :avocado do
3
+ resource :email, only: %i[edit update]
4
+ resource :password, only: %i[edit update]
5
+ resources :affirmations, only: %i[new show create]
6
+ resources :events, only: %i[index]
3
7
  resources :recoveries, only: %i[new create edit update]
4
8
  resources :registrations, only: %i[new create]
5
9
  resources :sessions, only: %i[index new create destroy]
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Current < ActiveSupport::CurrentAttributes
4
- attribute :session,
5
- :user,
6
- :user_agent,
7
- :ip_address
3
+ module Avocado
4
+ class Current < ActiveSupport::CurrentAttributes
5
+ attribute :session,
6
+ :user,
7
+ :user_agent,
8
+ :ip_address
8
9
 
9
- def session=(session)
10
- super
11
- self.user = session.user
10
+ def session=(session)
11
+ super
12
+ self.user = session.user
13
+ end
12
14
  end
13
15
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avocado
4
+ module Event
5
+ extend ActiveSupport::Concern
6
+
7
+ VALID_ACTIONS = [
8
+ "email:update",
9
+ "email:verified",
10
+ "password:update",
11
+ "session:create",
12
+ "session:destroy"
13
+ ]
14
+
15
+ included do
16
+ belongs_to :user
17
+
18
+ scope :newest_first, -> { order(created_at: :desc) }
19
+
20
+ validates :action, inclusion: VALID_ACTIONS
21
+
22
+ before_create :capture_request_details
23
+ end
24
+
25
+ private
26
+
27
+ def capture_request_details
28
+ self.user_agent = Current.user_agent
29
+ self.ip_address = Current.ip_address
30
+ end
31
+ end
32
+ end
@@ -5,9 +5,12 @@ module Avocado
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
+ include SessionCallbacks
9
+
8
10
  belongs_to :user
9
11
 
10
12
  scope :newest_first, -> { order(created_at: :desc) }
13
+ scope :non_current, -> { where.not(id: Current.session) }
11
14
  end
12
15
  end
13
16
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avocado
4
+ module SessionCallbacks
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ after_create :record_activity_create
9
+
10
+ after_destroy :record_activity_destroy
11
+
12
+ before_create :capture_request_details
13
+ end
14
+
15
+ private
16
+
17
+ def record_activity_create
18
+ create_user_event "session:create"
19
+ end
20
+
21
+ def record_activity_destroy
22
+ create_user_event "session:destroy"
23
+ end
24
+
25
+ def create_user_event(action)
26
+ user.events.create! action: action
27
+ end
28
+
29
+ def capture_request_details
30
+ self.user_agent = Current.user_agent
31
+ self.ip_address = Current.ip_address
32
+ end
33
+ end
34
+ end
data/lib/avocado/user.rb CHANGED
@@ -5,14 +5,21 @@ module Avocado
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- include UserEmail
8
+ include UserCallbacks
9
9
  include UserTokens
10
- include UserPassword
10
+ include UserValidations
11
11
 
12
- has_many :sessions
12
+ has_secure_password
13
+
14
+ with_options dependent: :destroy do
15
+ has_many :events
16
+ has_many :sessions
17
+ end
13
18
 
14
19
  scope :newest_first, -> { order(created_at: :desc) }
15
20
  scope :verified, -> { where(verified: true) }
21
+
22
+ normalizes :email, with: ->(email) { email.downcase.strip }
16
23
  end
17
24
  end
18
25
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avocado
4
+ module UserCallbacks
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ with_options if: :password_digest_previously_changed? do
9
+ after_update :destroy_non_current_sessions
10
+ after_update :record_activity_password_update
11
+ end
12
+
13
+ after_update :record_activity_email_update, if: :email_previously_changed?
14
+ after_update :record_activity_email_verified, if: %i[verified_previously_changed? verified?]
15
+
16
+ before_validation :remove_email_verification, if: :email_changed?, on: :update
17
+ end
18
+
19
+ private
20
+
21
+ def record_activity_password_update
22
+ create_event "password:update"
23
+ end
24
+
25
+ def record_activity_email_update
26
+ create_event "email:update"
27
+ end
28
+
29
+ def record_activity_email_verified
30
+ create_event "email:verified"
31
+ end
32
+
33
+ def create_event(action)
34
+ events.create! action: action
35
+ end
36
+
37
+ def remove_email_verification
38
+ self.verified = false
39
+ end
40
+
41
+ def destroy_non_current_sessions
42
+ sessions.non_current.destroy_all
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avocado
4
+ module UserValidations
5
+ extend ActiveSupport::Concern
6
+
7
+ PASSWORD_FORMAT = /\A(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).*\z/x
8
+ PASSWORD_MINIMUM_LENGTH = 8
9
+
10
+ included do
11
+ validates :email,
12
+ presence: true,
13
+ uniqueness: true,
14
+ format: {with: URI::MailTo::EMAIL_REGEXP}
15
+
16
+ validates :password,
17
+ format: {with: PASSWORD_FORMAT},
18
+ length: {minimum: PASSWORD_MINIMUM_LENGTH},
19
+ allow_nil: true
20
+ end
21
+ end
22
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Avocado
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/avocado.rb CHANGED
@@ -7,10 +7,12 @@ module Avocado
7
7
 
8
8
  autoload :Authentication, "avocado/authentication"
9
9
  autoload :Current, "avocado/current"
10
+ autoload :Event, "avocado/event"
10
11
  autoload :Mailer, "avocado/mailer"
11
12
  autoload :Session, "avocado/session"
13
+ autoload :SessionCallbacks, "avocado/session_callbacks"
12
14
  autoload :User, "avocado/user"
13
- autoload :UserEmail, "avocado/user_email"
15
+ autoload :UserCallbacks, "avocado/user_callbacks"
14
16
  autoload :UserTokens, "avocado/user_tokens"
15
- autoload :UserPassword, "avocado/user_password"
17
+ autoload :UserValidations, "avocado/user_validations"
16
18
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: avocado
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Jankowski
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-07-19 00:00:00.000000000 Z
11
+ date: 2023-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bcrypt
@@ -80,14 +80,23 @@ files:
80
80
  - LICENSE.txt
81
81
  - README.md
82
82
  - Rakefile
83
+ - app/controllers/avocado/affirmations_controller.rb
83
84
  - app/controllers/avocado/base_controller.rb
85
+ - app/controllers/avocado/emails_controller.rb
86
+ - app/controllers/avocado/events_controller.rb
87
+ - app/controllers/avocado/passwords_controller.rb
84
88
  - app/controllers/avocado/recoveries_controller.rb
85
89
  - app/controllers/avocado/registrations_controller.rb
86
90
  - app/controllers/avocado/sessions_controller.rb
87
91
  - app/controllers/avocado/verifications_controller.rb
92
+ - app/views/avocado/affirmations/new.html.erb
93
+ - app/views/avocado/emails/edit.html.erb
94
+ - app/views/avocado/events/_event.html.erb
95
+ - app/views/avocado/events/index.html.erb
88
96
  - app/views/avocado/mailer/email_affirmation.text.erb
89
97
  - app/views/avocado/mailer/email_verification.text.erb
90
98
  - app/views/avocado/mailer/password_reset.text.erb
99
+ - app/views/avocado/passwords/edit.html.erb
91
100
  - app/views/avocado/recoveries/edit.html.erb
92
101
  - app/views/avocado/recoveries/new.html.erb
93
102
  - app/views/avocado/registrations/new.html.erb
@@ -100,12 +109,14 @@ files:
100
109
  - lib/avocado/authentication.rb
101
110
  - lib/avocado/current.rb
102
111
  - lib/avocado/engine.rb
112
+ - lib/avocado/event.rb
103
113
  - lib/avocado/mailer.rb
104
114
  - lib/avocado/session.rb
115
+ - lib/avocado/session_callbacks.rb
105
116
  - lib/avocado/user.rb
106
- - lib/avocado/user_email.rb
107
- - lib/avocado/user_password.rb
117
+ - lib/avocado/user_callbacks.rb
108
118
  - lib/avocado/user_tokens.rb
119
+ - lib/avocado/user_validations.rb
109
120
  - lib/avocado/version.rb
110
121
  - sig/avocado.rbs
111
122
  homepage: https://github.com/tcuwp/avocado
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Avocado
4
- module UserEmail
5
- extend ActiveSupport::Concern
6
-
7
- included do
8
- validates :email, presence: true, uniqueness: true, format: {with: URI::MailTo::EMAIL_REGEXP}
9
-
10
- normalizes :email, with: ->(email) { email.downcase.strip }
11
- end
12
- end
13
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Avocado
4
- module UserPassword
5
- extend ActiveSupport::Concern
6
-
7
- REQUIRED_FORMAT = /\A(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).*\z/x
8
- REQUIRED_LENGTH = 8
9
-
10
- included do
11
- has_secure_password
12
-
13
- validates :password, format: {with: REQUIRED_FORMAT}, length: {minimum: REQUIRED_LENGTH}, allow_nil: true
14
- end
15
- end
16
- end