pages_core 3.13.0 → 3.14.0

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