pages_core 3.13.0 → 3.14.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/app/assets/builds/pages_core/admin-dist.js +1 -1
  4. data/app/assets/builds/pages_core/admin-dist.js.map +4 -4
  5. data/app/assets/builds/pages_core/admin.css +27 -4
  6. data/app/assets/stylesheets/pages_core/admin/components/login.css +0 -6
  7. data/app/assets/stylesheets/pages_core/admin/components/totp.css +26 -0
  8. data/app/controllers/admin/account_recoveries_controller.rb +87 -0
  9. data/app/controllers/admin/invites_controller.rb +3 -2
  10. data/app/controllers/admin/otp_secrets_controller.rb +45 -0
  11. data/app/controllers/admin/recovery_codes_controller.rb +32 -0
  12. data/app/controllers/admin/sessions_controller.rb +65 -0
  13. data/app/controllers/admin/users_controller.rb +2 -8
  14. data/app/controllers/concerns/pages_core/authentication.rb +12 -10
  15. data/app/controllers/pages_core/admin_controller.rb +1 -1
  16. data/app/helpers/pages_core/admin/admin_helper.rb +11 -0
  17. data/app/javascript/index.ts +0 -2
  18. data/app/mailers/admin_mailer.rb +2 -2
  19. data/app/models/concerns/pages_core/has_otp.rb +27 -0
  20. data/app/models/otp_secret.rb +101 -0
  21. data/app/models/user.rb +15 -37
  22. data/app/policies/user_policy.rb +4 -0
  23. data/app/views/admin/account_recoveries/new.html.erb +22 -0
  24. data/app/views/admin/account_recoveries/show.html.erb +37 -0
  25. data/app/views/admin/invites/show.html.erb +1 -1
  26. data/app/views/admin/otp_secrets/create.html.erb +7 -0
  27. data/app/views/admin/otp_secrets/new.html.erb +60 -0
  28. data/app/views/admin/recovery_codes/_codes.html.erb +14 -0
  29. data/app/views/admin/recovery_codes/create.html.erb +7 -0
  30. data/app/views/admin/recovery_codes/new.html.erb +11 -0
  31. data/app/views/admin/sessions/_otp_form.html.erb +13 -0
  32. data/app/views/admin/sessions/new.html.erb +33 -0
  33. data/app/views/admin/sessions/verify_otp.html.erb +19 -0
  34. data/app/views/admin/users/edit.html.erb +31 -1
  35. data/app/views/admin/users/new.html.erb +1 -1
  36. data/app/views/admin_mailer/account_recovery.text.erb +10 -0
  37. data/app/views/layouts/admin/_header.html.erb +1 -1
  38. data/app/views/layouts/admin/_toast.html.erb +12 -0
  39. data/app/views/layouts/admin.html.erb +1 -1
  40. data/config/locales/en.yml +11 -3
  41. data/config/routes.rb +11 -6
  42. data/db/migrate/20240126160700_add_2fa_fields.rb +22 -0
  43. data/db/migrate/20240129201300_remove_password_reset_tokens.rb +13 -0
  44. data/lib/pages_core.rb +6 -0
  45. metadata +51 -9
  46. data/app/controllers/admin/password_resets_controller.rb +0 -85
  47. data/app/controllers/sessions_controller.rb +0 -27
  48. data/app/javascript/controllers/LoginController.ts +0 -32
  49. data/app/models/password_reset_token.rb +0 -34
  50. data/app/views/admin/password_resets/show.html.erb +0 -21
  51. data/app/views/admin/users/login.html.erb +0 -65
  52. data/app/views/admin_mailer/password_reset.text.erb +0 -11
@@ -8370,10 +8370,6 @@ main .login-form ul li {
8370
8370
  font-size: 1.2em;
8371
8371
  }
8372
8372
 
8373
- .login-tab.hidden {
8374
- display: none;
8375
- }
8376
-
8377
8373
  body.modal > .wrapper {
8378
8374
  transition: filter 1000ms linear;
8379
8375
  filter: blur(10px);
@@ -9081,6 +9077,33 @@ textarea.rich {
9081
9077
  box-shadow: 0px 1px 1px #fff;
9082
9078
  }
9083
9079
 
9080
+ .totp-enrollment .qr-code {
9081
+ width: 20rem;
9082
+ height: 20rem;
9083
+ border: 1px solid #ddd;
9084
+ border: 1px solid var(--border-color);
9085
+ padding: 1rem;
9086
+ border-radius: 5px;
9087
+ }
9088
+
9089
+ .totp-enrollment .qr-code svg {
9090
+ width: 100%;
9091
+ height: 100%;
9092
+ }
9093
+
9094
+ ul.recovery-codes {
9095
+ list-style-type: none;
9096
+ padding: 0rem;
9097
+ margin: 2rem 0rem;
9098
+ font-family: monospace;
9099
+ display: flex;
9100
+ flex-direction: column;
9101
+ align-items: flex-start;
9102
+ gap: 1rem;
9103
+ font-size: 1.2em;
9104
+ font-weight: bold;
9105
+ }
9106
+
9084
9107
  .drag-handle {
9085
9108
  cursor: pointer;
9086
9109
  float: left;
@@ -25,9 +25,3 @@ main .login-form {
25
25
  }
26
26
  }
27
27
  }
28
-
29
- .login-tab {
30
- &.hidden {
31
- display: none;
32
- }
33
- }
@@ -0,0 +1,26 @@
1
+ .totp-enrollment {
2
+ .qr-code {
3
+ width: 20rem;
4
+ height: 20rem;
5
+ border: 1px solid var(--border-color);
6
+ padding: 1rem;
7
+ border-radius: 5px;
8
+ svg {
9
+ width: 100%;
10
+ height: 100%;
11
+ }
12
+ }
13
+ }
14
+
15
+ ul.recovery-codes {
16
+ list-style-type: none;
17
+ padding: 0rem;
18
+ margin: 2rem 0rem;
19
+ font-family: monospace;
20
+ display: flex;
21
+ flex-direction: column;
22
+ align-items: flex-start;
23
+ gap: 1rem;
24
+ font-size: 1.2em;
25
+ font-weight: bold;
26
+ }
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Admin
4
+ class AccountRecoveriesController < Admin::AdminController
5
+ before_action :require_authentication, except: %i[new create show update]
6
+ before_action :find_by_token, only: %i[show update]
7
+ around_action :validate_otp, only: %i[update]
8
+
9
+ def show; end
10
+
11
+ def new; end
12
+
13
+ def create
14
+ @user = User.find_by_email(params[:email])
15
+ if @user
16
+ deliver_account_recovery(@user)
17
+ flash[:notice] = t("pages_core.account_recovery.sent")
18
+ else
19
+ flash[:notice] = t("pages_core.account_recovery.not_found")
20
+ end
21
+ redirect_to admin_login_url
22
+ end
23
+
24
+ def update
25
+ if user_params[:password].present? && @user.update(user_params)
26
+ authenticate!(@user)
27
+ flash[:notice] = t("pages_core.account_recovery.changed")
28
+ redirect_to admin_login_url
29
+ else
30
+ render action: :show
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def deliver_account_recovery(user)
37
+ AdminMailer.account_recovery(
38
+ user,
39
+ admin_account_recovery_with_token_url(recovery_token(user))
40
+ ).deliver_later
41
+ end
42
+
43
+ def fail_recovery(message)
44
+ flash[:notice] = message
45
+ redirect_to new_admin_account_recovery_url
46
+ end
47
+
48
+ def find_by_token
49
+ @token = params[:token]
50
+ @user = User.find(message_verifier.verify(@token)[:id])
51
+ return if @user
52
+
53
+ fail_recovery(t("pages_core.account_recovery.invalid_request"))
54
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
55
+ fail_recovery(t("pages_core.account_recovery.invalid_request"))
56
+ end
57
+
58
+ def message_verifier
59
+ Rails.application.message_verifier(:account_recovery)
60
+ end
61
+
62
+ def recovery_token(user)
63
+ message_verifier.generate({ id: user.id }, expires_in: 24.hours)
64
+ end
65
+
66
+ def user_params
67
+ params.require(:user).permit(:password, :password_confirmation)
68
+ end
69
+
70
+ def validate_otp
71
+ User.transaction do
72
+ if valid_otp(@user, params[:otp])
73
+ yield
74
+ else
75
+ flash.now[:notice] = t("pages_core.otp.invalid_code")
76
+ render action: :show
77
+ end
78
+ end
79
+ end
80
+
81
+ def valid_otp(user, otp)
82
+ return true unless user.otp_enabled?
83
+
84
+ OtpSecret.new(user).validate_otp_or_recovery_code!(otp)
85
+ end
86
+ end
87
+ end
@@ -62,11 +62,12 @@ module Admin
62
62
  return if @invite && secure_compare(@invite.token, params[:token])
63
63
 
64
64
  flash[:notice] = t("pages_core.invite_expired")
65
- redirect_to(login_admin_users_url) && return
65
+ redirect_to(admin_login_url)
66
66
  end
67
67
 
68
68
  def user_params
69
- params.require(:user).permit(:name, :email, :password, :confirm_password)
69
+ params.require(:user)
70
+ .permit(:name, :email, :password, :password_confirmation)
70
71
  end
71
72
 
72
73
  def invite_params
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Admin
4
+ class OtpSecretsController < Admin::AdminController
5
+ before_action :require_otp_disabled, only: %i[new create]
6
+ before_action :find_otp_secret
7
+
8
+ def new
9
+ @otp_secret.generate
10
+ end
11
+
12
+ def create
13
+ if @otp_secret.verify(otp_secret_params)
14
+ @recovery_codes = @otp_secret.generate_recovery_codes
15
+ @otp_secret.enable!(@recovery_codes)
16
+ else
17
+ flash[:error] = t("pages_core.otp.invalid_code")
18
+ redirect_to new_admin_otp_secret_path
19
+ end
20
+ end
21
+
22
+ def destroy
23
+ @otp_secret.disable!
24
+ flash[:notice] = t("pages_core.otp.disabled")
25
+ redirect_to edit_admin_user_path(current_user)
26
+ end
27
+
28
+ private
29
+
30
+ def find_otp_secret
31
+ @otp_secret = OtpSecret.new(current_user)
32
+ end
33
+
34
+ def otp_secret_params
35
+ params.permit(:signed_message, :otp)
36
+ end
37
+
38
+ def require_otp_disabled
39
+ return unless current_user.otp_enabled?
40
+
41
+ flash[:notice] = t("pages_core.otp.already_enabled")
42
+ redirect_to edit_admin_user_path(current_user)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Admin
4
+ class RecoveryCodesController < Admin::AdminController
5
+ before_action :require_otp_enabled
6
+ before_action :find_otp_secret
7
+
8
+ def new; end
9
+
10
+ def create
11
+ if @otp_secret.validate_otp!(params[:otp])
12
+ @recovery_codes = @otp_secret.regenerate_recovery_codes!
13
+ else
14
+ flash[:error] = t("pages_core.otp.invalid_code")
15
+ redirect_to new_admin_recovery_codes_path
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def find_otp_secret
22
+ @otp_secret = OtpSecret.new(current_user)
23
+ end
24
+
25
+ def require_otp_enabled
26
+ return if current_user.otp_enabled?
27
+
28
+ flash[:notice] = t("pages_core.otp.required")
29
+ redirect_to edit_admin_user_path(current_user)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Admin
4
+ class SessionsController < Admin::AdminController
5
+ before_action :require_authentication, only: %i[destroy]
6
+ before_action :find_user, only: %i[create]
7
+ before_action :find_signed_user, only: %i[verify_otp]
8
+ before_action :require_user, only: %i[create verify_otp]
9
+
10
+ def new
11
+ redirect_to admin_default_url if logged_in?
12
+ end
13
+
14
+ def create
15
+ if @user.otp_enabled?
16
+ @signed_user_id = message_verifier.generate(
17
+ @user.id, expires_in: 1.hour
18
+ )
19
+ render template: "admin/sessions/verify_otp"
20
+ else
21
+ authenticate!(@user)
22
+ redirect_to admin_default_url
23
+ end
24
+ end
25
+
26
+ def destroy
27
+ flash[:notice] = t("pages_core.logged_out")
28
+ deauthenticate!
29
+ redirect_to admin_login_url
30
+ end
31
+
32
+ def verify_otp
33
+ @otp_secret = OtpSecret.new(@user)
34
+ if @otp_secret.validate_otp!(params[:otp])
35
+ authenticate!(@user)
36
+ redirect_to admin_default_url
37
+ else
38
+ flash[:notice] = t("pages_core.otp.invalid_code")
39
+ render template: "admin/sessions/verify_otp"
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def find_signed_user
46
+ @signed_user_id = params[:signed_user_id]
47
+ @user = User.find(message_verifier.verify(@signed_user_id))
48
+ end
49
+
50
+ def find_user
51
+ @user = User.authenticate(params[:email], password: params[:password])
52
+ end
53
+
54
+ def message_verifier
55
+ Rails.application.message_verifier(:session)
56
+ end
57
+
58
+ def require_user
59
+ return if @user
60
+
61
+ flash[:notice] = t("pages_core.invalid_login")
62
+ redirect_to admin_login_url
63
+ end
64
+ end
65
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Admin
4
4
  class UsersController < Admin::AdminController
5
- before_action :require_authentication, except: %i[new create login]
5
+ before_action :require_authentication, except: %i[new create]
6
6
  before_action :require_no_users, only: %i[new create]
7
7
  before_action(
8
8
  :find_user,
@@ -19,12 +19,6 @@ module Admin
19
19
  @invites = []
20
20
  end
21
21
 
22
- def login
23
- return unless logged_in?
24
-
25
- redirect_to admin_default_url
26
- end
27
-
28
22
  def show; end
29
23
 
30
24
  def new
@@ -81,7 +75,7 @@ module Admin
81
75
  { role_names: [] }]
82
76
  end
83
77
  if User.none? || (@user && policy(@user).change_password?)
84
- permitted_params += %i[password confirm_password]
78
+ permitted_params += %i[password password_confirmation]
85
79
  end
86
80
  params.require(:user).permit(permitted_params)
87
81
  end
@@ -35,20 +35,22 @@ module PagesCore
35
35
  @current_user = user
36
36
  end
37
37
 
38
- def start_authenticated_session
39
- if session[:current_user_id]
40
- user = User.where(id: session[:current_user_id]).first
41
- end
42
-
43
- return unless user&.can_login?
38
+ def finalize_authenticated_session
39
+ return unless logged_in?
44
40
 
45
- authenticated(user)
41
+ session[:current_user] =
42
+ { id: current_user.id, token: current_user.session_token }
46
43
  end
47
44
 
48
- def finalize_authenticated_session
49
- return unless current_user
45
+ def start_authenticated_session
46
+ user_session = session.fetch(:current_user, nil)&.symbolize_keys
47
+
48
+ return unless user_session
50
49
 
51
- session[:current_user_id] = current_user.id
50
+ user = User.find_by(id: user_session[:id])
51
+ return unless user && user.session_token == user_session[:token]
52
+
53
+ authenticated(user)
52
54
  end
53
55
  end
54
56
  end
@@ -48,7 +48,7 @@ module PagesCore
48
48
  if User.count < 1
49
49
  redirect_to(new_admin_user_url)
50
50
  else
51
- redirect_to(login_admin_users_url)
51
+ redirect_to(admin_login_url)
52
52
  end
53
53
  end
54
54
 
@@ -33,6 +33,17 @@ module PagesCore
33
33
  %w[January February March April May June July August September October
34
34
  November December][month - 1]
35
35
  end
36
+
37
+ def qr_code(url)
38
+ ActiveSupport::SafeBuffer.new(
39
+ RQRCode::QRCode.new(url)
40
+ .as_svg({ color: "000",
41
+ shape_rendering: "crispEdges",
42
+ module_size: 10,
43
+ use_path: true,
44
+ viewbox: true })
45
+ )
46
+ end
36
47
  end
37
48
  end
38
49
  end
@@ -7,7 +7,6 @@ import * as Components from "./components";
7
7
 
8
8
  import EditPageController from "./controllers/EditPageController";
9
9
  import MainController from "./controllers/MainController";
10
- import LoginController from "./controllers/LoginController";
11
10
  import PageOptionsController from "./controllers/PageOptionsController";
12
11
 
13
12
  import RichText from "./features/RichText";
@@ -26,7 +25,6 @@ export default function startPages() {
26
25
  const application = Application.start();
27
26
  application.register("edit-page", EditPageController);
28
27
  application.register("main", MainController);
29
- application.register("login", LoginController);
30
28
  application.register("page-options", PageOptionsController);
31
29
  }
32
30
 
@@ -4,12 +4,12 @@ class AdminMailer < ApplicationMailer
4
4
  default from: proc { "\"Pages\" <no-reply@anyone.no>" }
5
5
  layout false
6
6
 
7
- def password_reset(user, url)
7
+ def account_recovery(user, url)
8
8
  @user = user
9
9
  @url = url
10
10
  mail(
11
11
  to: @user.email,
12
- subject: "Reset your password on #{PagesCore.config(:site_name)}"
12
+ subject: "Recover your account on #{PagesCore.config(:site_name)}"
13
13
  )
14
14
  end
15
15
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PagesCore
4
+ module HasOtp
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ validates :otp_secret, presence: true, if: :otp_enabled?
9
+ end
10
+
11
+ def recovery_codes=(codes)
12
+ self.hashed_recovery_codes = codes.map do |c|
13
+ BCrypt::Password.create(c, cost: 8)
14
+ end
15
+ end
16
+
17
+ def use_recovery_code!(code)
18
+ valid_hashes = hashed_recovery_codes.select do |c|
19
+ BCrypt::Password.new(c) == code
20
+ end
21
+ return false unless valid_hashes.any?
22
+
23
+ update(hashed_recovery_codes: hashed_recovery_codes - valid_hashes)
24
+ true
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ class OtpSecret
4
+ attr_reader :user, :secret
5
+
6
+ def initialize(user)
7
+ @user = user
8
+ @secret = user.otp_secret
9
+ end
10
+
11
+ def account_name
12
+ user.email
13
+ end
14
+
15
+ def disable!
16
+ user.update(otp_enabled: false,
17
+ otp_secret: nil,
18
+ last_otp_at: nil,
19
+ recovery_codes: [])
20
+ end
21
+
22
+ def enable!(recovery_codes)
23
+ user.update(otp_enabled: true,
24
+ otp_secret: secret,
25
+ last_otp_at: Time.zone.now,
26
+ recovery_codes:)
27
+ end
28
+
29
+ def generate
30
+ @secret = ROTP::Base32.random
31
+ end
32
+
33
+ def generate_recovery_codes
34
+ 10.times.map { SecureRandom.alphanumeric(16) }
35
+ end
36
+
37
+ def provisioning_uri
38
+ totp.provisioning_uri(account_name)
39
+ end
40
+
41
+ def regenerate_recovery_codes!
42
+ generate_recovery_codes.tap do |recovery_codes|
43
+ user.update(recovery_codes:)
44
+ end
45
+ end
46
+
47
+ def signed_message
48
+ message_verifier.generate(
49
+ { user_id: user.id, secret: }, expires_in: 1.hour
50
+ )
51
+ end
52
+
53
+ def validate_otp!(code)
54
+ return false unless valid_otp?(code)
55
+
56
+ user.update(last_otp_at: Time.zone.now)
57
+ true
58
+ end
59
+
60
+ def validate_otp_or_recovery_code!(code)
61
+ if code =~ /^[\d]{6}$/
62
+ validate_otp!(code)
63
+ else
64
+ validate_recovery_code!(code)
65
+ end
66
+ end
67
+
68
+ def validate_recovery_code!(code)
69
+ user.use_recovery_code!(code)
70
+ end
71
+
72
+ def verify(params)
73
+ @secret = verify_secret(params[:signed_message])
74
+ valid_otp?(params[:otp])
75
+ end
76
+
77
+ private
78
+
79
+ def message_verifier
80
+ Rails.application.message_verifier(:otp_secret)
81
+ end
82
+
83
+ def totp
84
+ ROTP::TOTP.new(secret)
85
+ end
86
+
87
+ def valid_otp?(otp)
88
+ if user.otp_enabled?
89
+ totp.verify(otp, after: user.last_otp_at, drift_behind: 10)
90
+ else
91
+ totp.verify(otp, drift_behind: 10)
92
+ end
93
+ end
94
+
95
+ def verify_secret(signed)
96
+ payload = message_verifier.verify(signed)
97
+ raise "Wrong user" unless payload[:user_id] == user.id
98
+
99
+ payload[:secret]
100
+ end
101
+ end
data/app/models/user.rb CHANGED
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class User < ApplicationRecord
4
+ include PagesCore::HasOtp
4
5
  include PagesCore::HasRoles
5
6
 
6
- attr_accessor :password, :confirm_password
7
+ has_secure_password
7
8
 
8
9
  belongs_to(:creator,
9
10
  class_name: "User",
@@ -16,7 +17,6 @@ class User < ApplicationRecord
16
17
  dependent: :nullify,
17
18
  inverse_of: :creator)
18
19
  has_many :pages, dependent: :nullify
19
- has_many :password_reset_tokens, dependent: :destroy
20
20
  has_many :roles, dependent: :destroy
21
21
  has_many :invites, dependent: :destroy
22
22
  belongs_to_image :image, foreign_key: :image_id, optional: true
@@ -30,12 +30,14 @@ class User < ApplicationRecord
30
30
  format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i },
31
31
  uniqueness: { case_sensitive: false }
32
32
 
33
- validates :password, presence: true, on: :create
34
- validates :password, length: { minimum: 8 }, allow_blank: true
33
+ validates :password,
34
+ length: {
35
+ minimum: 8,
36
+ maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED,
37
+ allow_blank: true
38
+ }
35
39
 
36
- validate :confirm_password_must_match
37
-
38
- before_validation :hash_password
40
+ before_save :update_session_token
39
41
  before_create :ensure_first_user_has_all_roles
40
42
 
41
43
  scope :by_name, -> { order("name ASC") }
@@ -44,12 +46,11 @@ class User < ApplicationRecord
44
46
 
45
47
  class << self
46
48
  def authenticate(email, password:)
47
- user = User.find_by_email(email)
48
- user if user.try { |u| u.authenticate!(password) }
49
+ User.find_by_email(email).try(:authenticate, password)
49
50
  end
50
51
 
51
52
  def find_by_email(str)
52
- find_by("LOWER(email) = ?", str.to_s.downcase)
53
+ find_by("LOWER(email) = ?", str.to_s.downcase.strip)
53
54
  end
54
55
  end
55
56
 
@@ -84,16 +85,6 @@ class User < ApplicationRecord
84
85
 
85
86
  private
86
87
 
87
- def confirm_password_must_match
88
- return if password.blank? || password == confirm_password
89
-
90
- errors.add(:confirm_password, "does not match")
91
- end
92
-
93
- def encrypt_password(password)
94
- BCrypt::Password.create(password)
95
- end
96
-
97
88
  def ensure_first_user_has_all_roles
98
89
  return if User.any?
99
90
 
@@ -103,23 +94,10 @@ class User < ApplicationRecord
103
94
  end
104
95
  end
105
96
 
106
- def hash_password
107
- self.hashed_password = encrypt_password(password) if password.present?
108
- end
109
-
110
- def password_needs_rehash?
111
- hashed_password.length <= 40
112
- end
97
+ def update_session_token
98
+ return unless !session_token? || password_digest_changed? ||
99
+ otp_enabled_changed?
113
100
 
114
- def rehash_password!(password)
115
- update(hashed_password: encrypt_password(password))
116
- end
117
-
118
- def valid_password?(password)
119
- if hashed_password.length <= 40
120
- hashed_password == Digest::SHA1.hexdigest(password)
121
- else
122
- BCrypt::Password.new(hashed_password) == password
123
- end
101
+ self.session_token = SecureRandom.hex(32)
124
102
  end
125
103
  end
@@ -48,4 +48,8 @@ class UserPolicy < Policy
48
48
  def change_password?
49
49
  user == record
50
50
  end
51
+
52
+ def otp?
53
+ user == record
54
+ end
51
55
  end