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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/app/assets/builds/pages_core/admin-dist.js +1 -1
- data/app/assets/builds/pages_core/admin-dist.js.map +4 -4
- data/app/assets/builds/pages_core/admin.css +27 -4
- data/app/assets/stylesheets/pages_core/admin/components/login.css +0 -6
- data/app/assets/stylesheets/pages_core/admin/components/totp.css +26 -0
- data/app/controllers/admin/account_recoveries_controller.rb +87 -0
- data/app/controllers/admin/invites_controller.rb +3 -2
- data/app/controllers/admin/otp_secrets_controller.rb +45 -0
- data/app/controllers/admin/recovery_codes_controller.rb +32 -0
- data/app/controllers/admin/sessions_controller.rb +65 -0
- data/app/controllers/admin/users_controller.rb +2 -8
- data/app/controllers/concerns/pages_core/authentication.rb +12 -10
- data/app/controllers/pages_core/admin_controller.rb +1 -1
- data/app/helpers/pages_core/admin/admin_helper.rb +11 -0
- data/app/javascript/index.ts +0 -2
- data/app/mailers/admin_mailer.rb +2 -2
- data/app/models/concerns/pages_core/has_otp.rb +27 -0
- data/app/models/otp_secret.rb +101 -0
- data/app/models/user.rb +15 -37
- data/app/policies/user_policy.rb +4 -0
- data/app/views/admin/account_recoveries/new.html.erb +22 -0
- data/app/views/admin/account_recoveries/show.html.erb +37 -0
- data/app/views/admin/invites/show.html.erb +1 -1
- data/app/views/admin/otp_secrets/create.html.erb +7 -0
- data/app/views/admin/otp_secrets/new.html.erb +60 -0
- data/app/views/admin/recovery_codes/_codes.html.erb +14 -0
- data/app/views/admin/recovery_codes/create.html.erb +7 -0
- data/app/views/admin/recovery_codes/new.html.erb +11 -0
- data/app/views/admin/sessions/_otp_form.html.erb +13 -0
- data/app/views/admin/sessions/new.html.erb +33 -0
- data/app/views/admin/sessions/verify_otp.html.erb +19 -0
- data/app/views/admin/users/edit.html.erb +31 -1
- data/app/views/admin/users/new.html.erb +1 -1
- data/app/views/admin_mailer/account_recovery.text.erb +10 -0
- data/app/views/layouts/admin/_header.html.erb +1 -1
- data/app/views/layouts/admin/_toast.html.erb +12 -0
- data/app/views/layouts/admin.html.erb +1 -1
- data/config/locales/en.yml +11 -3
- data/config/routes.rb +11 -6
- data/db/migrate/20240126160700_add_2fa_fields.rb +22 -0
- data/db/migrate/20240129201300_remove_password_reset_tokens.rb +13 -0
- data/lib/pages_core.rb +6 -0
- metadata +51 -9
- data/app/controllers/admin/password_resets_controller.rb +0 -85
- data/app/controllers/sessions_controller.rb +0 -27
- data/app/javascript/controllers/LoginController.ts +0 -32
- data/app/models/password_reset_token.rb +0 -34
- data/app/views/admin/password_resets/show.html.erb +0 -21
- data/app/views/admin/users/login.html.erb +0 -65
- 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;
|
@@ -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(
|
65
|
+
redirect_to(admin_login_url)
|
66
66
|
end
|
67
67
|
|
68
68
|
def user_params
|
69
|
-
params.require(:user)
|
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
|
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
|
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
|
39
|
-
|
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
|
-
|
41
|
+
session[:current_user] =
|
42
|
+
{ id: current_user.id, token: current_user.session_token }
|
46
43
|
end
|
47
44
|
|
48
|
-
def
|
49
|
-
|
45
|
+
def start_authenticated_session
|
46
|
+
user_session = session.fetch(:current_user, nil)&.symbolize_keys
|
47
|
+
|
48
|
+
return unless user_session
|
50
49
|
|
51
|
-
|
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
|
@@ -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
|
data/app/javascript/index.ts
CHANGED
@@ -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
|
|
data/app/mailers/admin_mailer.rb
CHANGED
@@ -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
|
7
|
+
def account_recovery(user, url)
|
8
8
|
@user = user
|
9
9
|
@url = url
|
10
10
|
mail(
|
11
11
|
to: @user.email,
|
12
|
-
subject: "
|
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
|
-
|
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,
|
34
|
-
|
33
|
+
validates :password,
|
34
|
+
length: {
|
35
|
+
minimum: 8,
|
36
|
+
maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED,
|
37
|
+
allow_blank: true
|
38
|
+
}
|
35
39
|
|
36
|
-
|
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
|
-
|
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
|
107
|
-
|
108
|
-
|
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
|
-
|
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
|