trusty-cms 7.0.28 → 7.0.29

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/Gemfile.lock +15 -1
  4. data/INSTALL.md +21 -3
  5. data/app/assets/stylesheets/admin/modules/_buttons.scss +4 -1
  6. data/app/assets/stylesheets/admin/partials/_forms.scss +4 -0
  7. data/app/controllers/admin/security_controller.rb +74 -0
  8. data/app/controllers/admin/sessions_controller.rb +43 -0
  9. data/app/controllers/admin/two_factor_controller.rb +42 -0
  10. data/app/controllers/application_controller.rb +7 -0
  11. data/app/models/trusty_cms/config.rb +0 -4
  12. data/app/models/user.rb +1 -1
  13. data/app/views/admin/configuration/edit.html.haml +0 -7
  14. data/app/views/admin/configuration/show.html.haml +21 -12
  15. data/app/views/admin/preferences/edit.html.haml +1 -4
  16. data/app/views/admin/security/edit.html.haml +57 -0
  17. data/app/views/{devise → admin}/sessions/new.html.haml +5 -5
  18. data/app/views/admin/two_factor/show.html.haml +14 -0
  19. data/app/views/admin/users/_form.html.haml +0 -3
  20. data/app/views/devise/passwords/edit.html.haml +2 -4
  21. data/config/initializers/active_record_encryption.rb +5 -0
  22. data/config/initializers/devise.rb +4 -0
  23. data/config/initializers/trusty_cms_config.rb +0 -1
  24. data/config/locales/en.yml +29 -5
  25. data/config/routes.rb +8 -4
  26. data/db/migrate/20250502162215_add_devise_two_factor_to_admins.rb +7 -0
  27. data/lib/trusty_cms/admin_ui.rb +10 -5
  28. data/lib/trusty_cms/version.rb +1 -1
  29. data/package.json +1 -1
  30. data/spec/dummy/config/initializers/trusty_cms_config.rb +0 -1
  31. data/trusty_cms.gemspec +2 -0
  32. data/yarn.lock +4 -4
  33. metadata +38 -4
  34. data/app/views/admin/users/_password_fields.html.haml +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 16753e005b09f59c333c5e9fd2af4bca3bd0e20f8d2da054500b3d262d1eecfa
4
- data.tar.gz: c4159171a6a9f932b685cd3855bd0f931392053d4d797025e73aae1a5aa443fc
3
+ metadata.gz: 520f342b36983dbca58c14ba3b104f212ed80c271797d63081212d8945ca7ade
4
+ data.tar.gz: 8f0204809ddbd49f9d2db4dde486f92a05bd1da1f410b73764a6d79edab26b73
5
5
  SHA512:
6
- metadata.gz: 3f91892e8c3e8b0aa0e3899b7796833a4e2a0e5074eacf92f5e47b7e216559b5c4d32f487f03fb725775d960d20f0cd15cea678a0a14ef42aa65c9d07cb531c2
7
- data.tar.gz: f5735dc96aa6cf0c3c80686f36d48cd7d49f5b8865ffd4c2d02ab708170764b5bff91a7fb525acbff932e67c76b9adcd104f35e145ee71ce61c4c897e42c597f
6
+ metadata.gz: 31fd3cb0dcbf88a788b127ac94404e9b813f6af5e09a2533eceab1b7974c821f9eae10884704df8ba59ad3c4a3ee7b5885a8b65024dcda1cfe0f1d6374b8db8e
7
+ data.tar.gz: 5ba2f9d8b2bcbd0875d058a35e7035058beed413c835ae5ac3e66a579ecc9b14aad4a86714f8c12eaf16e877e4c5c88291260b4f243523193e8d7bfa0a9089e4
data/Gemfile CHANGED
@@ -15,6 +15,7 @@ group :development, :test do
15
15
  gem 'activestorage-validator'
16
16
  gem 'acts_as_list'
17
17
  gem 'database_cleaner'
18
+ gem 'devise-two-factor'
18
19
  gem 'factory_bot_rails', '6.4.4'
19
20
  gem 'file_validators'
20
21
  gem 'launchy', '~> 3.0.1'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- trusty-cms (7.0.28)
4
+ trusty-cms (7.0.29)
5
5
  RedCloth (= 4.3.3)
6
6
  activestorage-validator
7
7
  acts_as_list (>= 0.9.5, < 1.3.0)
@@ -11,6 +11,7 @@ PATH
11
11
  ckeditor (>= 4.2.2, < 4.4.0)
12
12
  delocalize (>= 0.2, < 2.0)
13
13
  devise
14
+ devise-two-factor
14
15
  drb
15
16
  execjs (~> 2.7)
16
17
  haml (>= 5.0, < 6.0)
@@ -32,6 +33,7 @@ PATH
32
33
  ransack (~> 4.2.1)
33
34
  rdoc (>= 5.1, < 7.0)
34
35
  roadie-rails
36
+ rqrcode
35
37
  sass-rails
36
38
  stringex (>= 2.7.1, < 2.9.0)
37
39
  tzinfo (>= 1.2.3, < 2.1.0)
@@ -133,6 +135,7 @@ GEM
133
135
  xpath (~> 3.2)
134
136
  childprocess (5.1.0)
135
137
  logger (~> 1.5)
138
+ chunky_png (1.4.0)
136
139
  ckeditor (4.3.0)
137
140
  orm_adapter (~> 0.5.0)
138
141
  terrapin
@@ -159,6 +162,11 @@ GEM
159
162
  railties (>= 4.1.0)
160
163
  responders
161
164
  warden (~> 1.2.3)
165
+ devise-two-factor (6.1.0)
166
+ activesupport (>= 7.0, < 8.1)
167
+ devise (~> 4.0)
168
+ railties (>= 7.0, < 8.1)
169
+ rotp (~> 6.0)
162
170
  diff-lcs (1.5.1)
163
171
  docile (1.4.1)
164
172
  drb (2.2.1)
@@ -346,6 +354,11 @@ GEM
346
354
  roadie-rails (3.3.0)
347
355
  railties (>= 5.1, < 8.1)
348
356
  roadie (~> 5.0)
357
+ rotp (6.3.0)
358
+ rqrcode (3.1.0)
359
+ chunky_png (~> 1.0)
360
+ rqrcode_core (~> 2.0)
361
+ rqrcode_core (2.0.0)
349
362
  rspec-core (3.13.2)
350
363
  rspec-support (~> 3.13.0)
351
364
  rspec-expectations (3.13.3)
@@ -430,6 +443,7 @@ DEPENDENCIES
430
443
  activestorage-validator
431
444
  acts_as_list
432
445
  database_cleaner
446
+ devise-two-factor
433
447
  factory_bot_rails (= 6.4.4)
434
448
  file_validators
435
449
  launchy (~> 3.0.1)
data/INSTALL.md CHANGED
@@ -6,8 +6,9 @@ From within the directory containing your TrustyCMS instance:
6
6
 
7
7
  2. Add the following gems to your Gemfile:
8
8
 
9
- - gem 'trusty-cms'
10
- - gem 'rails-observers'
9
+ - gem 'trusty-cms'
10
+ - gem 'rails-observers'
11
+ - gem 'devise-two-factor'
11
12
 
12
13
  3. Run `bundle install`
13
14
 
@@ -20,5 +21,22 @@ From within the directory containing your TrustyCMS instance:
20
21
 
21
22
  7. Add utf8 encoding to your db.yml
22
23
 
23
- 8. Run `bundle exec rake db:setup`, `bundle exec rake trusty_cms:install:migrations`, then
24
+ 8. Set up encryption keys required for Rails’ native encryption (used by features like two-factor authentication):
25
+
26
+ - Run the encryption initializer command:
27
+
28
+ ```bash
29
+ ./bin/rails db:encryption:init
30
+ ```
31
+
32
+ - This will output three secrets. Copy the values and set them as environment variables in your preferred environment file (e.g., `.env`, `.env.development`, or via system environment settings):
33
+
34
+ ```env
35
+ ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY
36
+ ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
37
+ ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
38
+ ```
39
+ - **Important**: Use different keys for each environment (development, test, production) unless two environments (e.g., development and staging) share a database — in that case, they **must** use the same keys.
40
+
41
+ 9. Run `bundle exec rake db:setup`, `bundle exec rake trusty_cms:install:migrations`, then
24
42
  `bundle exec rake db:bootstrap`.
@@ -18,7 +18,6 @@
18
18
  text-decoration: none;
19
19
  }
20
20
 
21
-
22
21
  .cancel-button {
23
22
  @include button;
24
23
  background-color: $light-red;
@@ -30,3 +29,7 @@
30
29
  color: $white;
31
30
  }
32
31
  }
32
+
33
+ .button-margin-top {
34
+ margin-top: 0.5em;
35
+ }
@@ -111,3 +111,7 @@ textarea {
111
111
  #search-input {
112
112
  max-width: 25em;
113
113
  }
114
+
115
+ .form-margin-top {
116
+ margin-top: 0.5em;
117
+ }
@@ -0,0 +1,74 @@
1
+ require 'rqrcode'
2
+
3
+ class Admin::SecurityController < ApplicationController
4
+ before_action :authenticate_user!
5
+ before_action :initialize_variables
6
+ before_action :initialize_two_factor_variables, only: %i[show edit update]
7
+
8
+ def show
9
+ set_standard_body_style
10
+ ensure_user_has_otp_secret
11
+ render :edit
12
+ end
13
+
14
+ def edit
15
+ render
16
+ end
17
+
18
+ def update
19
+ if @user.update(security_params)
20
+ sign_out(@user)
21
+ redirect_to new_user_session_path, notice: t('security_controller.password_updated')
22
+ else
23
+ flash[:error] = t('security_controller.error_updating_password')
24
+ render :edit
25
+ end
26
+ end
27
+
28
+ def verify_two_factor
29
+ if current_user.validate_and_consume_otp!(params[:otp_attempt])
30
+ current_user.update!(otp_required_for_login: true)
31
+ redirect_to admin_security_path, notice: t('security_controller.two_factor_enabled')
32
+ else
33
+ flash[:error] = t('security_controller.two_factor_invalid_code')
34
+ redirect_to admin_security_path
35
+ end
36
+ end
37
+
38
+ def disable_two_factor
39
+ if current_user.update!(otp_required_for_login: false)
40
+ redirect_to admin_security_path, notice: t('security_controller.two_factor_disabled')
41
+ else
42
+ flash[:error] = t('security_controller.two_factor_disabled_error')
43
+ redirect_to admin_security_path
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def initialize_variables
50
+ @user = current_user
51
+ @controller_name = 'user'
52
+ @template_name = 'security'
53
+ end
54
+
55
+ def ensure_user_has_otp_secret
56
+ return if current_user.otp_secret.present?
57
+
58
+ current_user.update!(otp_secret: User.generate_otp_secret)
59
+ end
60
+
61
+ def initialize_two_factor_variables
62
+ @two_factor_enabled = current_user.otp_required_for_login
63
+
64
+ unless @two_factor_enabled
65
+ otp_uri = current_user.otp_provisioning_uri(current_user.email, issuer: 'TrustyCMS')
66
+ qr = RQRCode::QRCode.new(otp_uri)
67
+ @qr_png_data = qr.as_png(size: 200).to_data_url
68
+ end
69
+ end
70
+
71
+ def security_params
72
+ params.require(:user).permit(:password, :password_confirmation)
73
+ end
74
+ end
@@ -0,0 +1,43 @@
1
+ class Admin::SessionsController < Devise::SessionsController
2
+ def create
3
+ user = find_user
4
+
5
+ if authenticated?(user)
6
+ handle_successful_authentication(user)
7
+ else
8
+ handle_failed_authentication
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def find_user
15
+ User.find_by(email: params[:user][:email])
16
+ end
17
+
18
+ def authenticated?(user)
19
+ user&.valid_password?(params[:user][:password])
20
+ end
21
+
22
+ def handle_successful_authentication(user)
23
+ if user.otp_required_for_login
24
+ start_two_factor_session(user)
25
+ redirect_to admin_two_factor_path
26
+ else
27
+ sign_in(:user, user)
28
+ redirect_to after_sign_in_path_for(user)
29
+ end
30
+ end
31
+
32
+ def handle_failed_authentication
33
+ self.resource = resource_class.new(sign_in_params)
34
+ clean_up_passwords(resource)
35
+ flash.now[:alert] = t('invalid_email_or_password')
36
+ render :new
37
+ end
38
+
39
+ def start_two_factor_session(user)
40
+ session[:pre_2fa_user_id] = user.id
41
+ session[:pre_2fa_started_at] = Time.current.to_i
42
+ end
43
+ end
@@ -0,0 +1,42 @@
1
+ class Admin::TwoFactorController < ApplicationController
2
+ skip_before_action :authenticate_user!
3
+ before_action :load_pre_2fa_user
4
+
5
+ MAX_2FA_SESSION_DURATION = 5.minutes.freeze
6
+
7
+ def show; end
8
+
9
+ def create
10
+ if @user.validate_and_consume_otp!(params[:otp_attempt])
11
+ session.delete(:pre_2fa_user_id)
12
+ session.delete(:pre_2fa_started_at)
13
+ sign_in(:user, @user)
14
+ redirect_to after_sign_in_path_for(@user)
15
+ else
16
+ reset_session
17
+ redirect_to new_user_session_path, alert: t('two_factor_controller.invalid_code')
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def load_pre_2fa_user
24
+ if current_user
25
+ redirect_to after_sign_in_path_for(current_user) and return
26
+ end
27
+
28
+ @user = User.find_by(id: session[:pre_2fa_user_id])
29
+
30
+ if !@user&.otp_required_for_login || session_expired?
31
+ reset_session
32
+ redirect_to new_user_session_path, alert: t('two_factor_controller.session_expired')
33
+ end
34
+ end
35
+
36
+ def session_expired?
37
+ started_at = session[:pre_2fa_started_at]
38
+ return true unless started_at
39
+
40
+ Time.current.to_i - started_at > MAX_2FA_SESSION_DURATION
41
+ end
42
+ end
@@ -6,6 +6,7 @@ class ApplicationController < ActionController::Base
6
6
 
7
7
  protect_from_forgery with: :exception
8
8
  before_action :authenticate_user!
9
+ before_action :configure_permitted_parameters, if: :devise_controller?
9
10
  before_action :set_timezone
10
11
  before_action :set_user_locale
11
12
  before_action :set_javascripts_and_stylesheets
@@ -44,6 +45,12 @@ class ApplicationController < ActionController::Base
44
45
  end
45
46
  end
46
47
 
48
+ protected
49
+
50
+ def configure_permitted_parameters
51
+ devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
52
+ end
53
+
47
54
  private
48
55
 
49
56
  def set_mailer
@@ -145,10 +145,6 @@ module TrustyCms
145
145
  @default_settings ||= %w{defaults.locale defaults.page.filter defaults.page.parts defaults.page.fields defaults.page.status}
146
146
  end
147
147
 
148
- def user_settings
149
- @user_settings ||= ['user.allow_password_reset?']
150
- end
151
-
152
148
  # A convenient drying method for specifying a prefix and options common to several settings.
153
149
  #
154
150
  # TrustyCms.config do |config|
data/app/models/user.rb CHANGED
@@ -3,7 +3,7 @@ class User < ActiveRecord::Base
3
3
  self.table_name = 'admins'
4
4
 
5
5
  # :confirmable, :lockable, :timeoutable and :omniauthable
6
- devise :database_authenticatable, :registerable,
6
+ devise :two_factor_authenticatable, :registerable,
7
7
  :recoverable, :rememberable, :trackable, :validatable
8
8
 
9
9
  alias_attribute :created_by_id, :id
@@ -23,13 +23,6 @@
23
23
  %p
24
24
  = edit_config default_setting
25
25
 
26
- - form.edit_users do
27
- %fieldset
28
- %h4 Passwords
29
- - TrustyCms.config.user_settings.each do |user_setting|
30
- %p
31
- = edit_config user_setting
32
-
33
26
  - render_region :form_bottom do |form_bottom|
34
27
  - form_bottom.edit_buttons do
35
28
  .buttons
@@ -1,4 +1,4 @@
1
- #preferences.box
1
+ %fieldset
2
2
  - render_region :user do |user|
3
3
  - user.preferences do
4
4
  %h3
@@ -13,10 +13,6 @@
13
13
  = t('email_address')
14
14
  %span.uri
15
15
  = current_user.email
16
- %p.ruled
17
- %label
18
- = t('password')
19
- %big &bull;&bull;&bull;&bull;&bull;
20
16
  %p.ruled
21
17
  %label
22
18
  = t('language')
@@ -25,7 +21,25 @@
25
21
  .actions
26
22
  = button_to t("edit_preferences"), edit_admin_user_path(current_user), :method => :get
27
23
 
28
- #config.box
24
+ %fieldset
25
+ - render_region :user do |user|
26
+ - user.preferences do
27
+ %h3
28
+ = t('security')
29
+ %p.ruled
30
+ %label
31
+ = t('password')
32
+ %span.value
33
+ &bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;
34
+ %p.ruled
35
+ %label
36
+ = t('two_factor_authentication')
37
+ %span
38
+ = current_user.otp_required_for_login ? t('enabled') : t('disabled')
39
+ .actions
40
+ = button_to t('edit_security_settings'), admin_security_path, :method => :get
41
+
42
+ %fieldset
29
43
  - render_region :trusty_config do |config|
30
44
  - config.site do
31
45
  %h3
@@ -40,11 +54,6 @@
40
54
  %p.ruled
41
55
  = show_config default_setting
42
56
 
43
- - config.users do
44
- %h4 Passwords
45
- - TrustyCms.config.user_settings.each do |user_setting|
46
- %p.ruled
47
- = show_config user_setting
48
57
  - if current_user.admin?
49
58
  .actions
50
- = button_to t("edit_configuration"), edit_admin_configuration_path, :method => :get
59
+ = button_to t("edit_configuration"), edit_admin_configuration_path, :method => :get
@@ -27,12 +27,9 @@
27
27
  = f.label :email, t("email_address"), :class => 'optional'
28
28
  = f.text_field 'email', :class => 'textbox', :maxlength => 255
29
29
 
30
- - form.edit_password do
31
- = render 'admin/users/password_fields', :f => f
32
-
33
30
  - render_region :form_bottom, :locals => {:f => f} do |form_bottom|
34
31
  - form_bottom.edit_buttons do
35
32
  .buttons
36
33
  = save_model_button @user
37
34
  = t('or')
38
- = link_to t('cancel'), admin_pages_url, class: 'alt'
35
+ = link_to t('cancel'), admin_configuration_path, class: 'alt'
@@ -0,0 +1,57 @@
1
+ - @page_title = @user.name + ' - Security'
2
+ - body_classes << 'edit_security'
3
+
4
+ - render_region :main do |main|
5
+ - main.edit_header do
6
+ %h1
7
+ = t('security')
8
+
9
+ - main.edit_form do
10
+ %fieldset
11
+ %h3
12
+ = t('change_password')
13
+ = form_for @user, :url => admin_security_path, :html => { :method => :put, 'data-onsubmit_status' => "#{t('saving_changes')}&#8230;" } do |f|
14
+ - render_region :form, :locals => {:f => f} do |form|
15
+ - form.edit_password do
16
+ %fieldset#change-password
17
+ %p
18
+ = f.label :password, t('new_password')
19
+ = f.password_field 'password', :value => '', :maxlength => 40, :autocomplete => 'new-password'
20
+ %p
21
+ = f.label :password_confirmation, t('password_confirmation')
22
+ = f.password_field 'password_confirmation', :value => '', :maxlength => 40, :autocomplete => 'new-password'
23
+
24
+ - render_region :form_bottom, :locals => {:f => f} do |form_bottom|
25
+ - form_bottom.edit_buttons do
26
+ .buttons
27
+ = save_model_button @user
28
+
29
+ - main.two_factor do
30
+ %fieldset
31
+ %h3
32
+ = t('two_factor_authentication')
33
+
34
+ - if @two_factor_enabled
35
+ %fieldset
36
+ %p
37
+ = t('security_controller.two_factor_is_enabled')
38
+ = button_to t('disable'),
39
+ disable_two_factor_admin_security_path,
40
+ method: :post,
41
+ data: { confirm: t('security_controller.two_factor_disable_confirm') },
42
+ class: "button button-margin-top"
43
+ - else
44
+ %fieldset
45
+ %p
46
+ = t('security_controller.scan_qr_instructions')
47
+ = image_tag @qr_png_data, alt: t('security_controller.qr_alt')
48
+ %p
49
+ = t('security_controller.manual_key_instructions')
50
+ %strong= current_user.otp_secret
51
+
52
+ .form-margin-top
53
+ = form_with url: verify_two_factor_admin_security_path, method: :post do |form|
54
+ %div
55
+ = form.label :otp_attempt, t('security_controller.enter_qr_code')
56
+ = form.text_field :otp_attempt, autofocus: true, size: 6, maxlength: 6
57
+ = form.submit t('security_controller.verify_and_enable')
@@ -1,10 +1,10 @@
1
1
  - body_classes << 'login_form'
2
2
  .login-form-content
3
3
  .visual
4
- = image_tag('/assets/admin/default_safe_login.svg', alt: 'web browser with padlock on top')
4
+ = image_tag('/assets/admin/default_safe_login.svg', alt: 'Web browser with padlock on top')
5
5
  .login
6
- %h1 Log in
7
- = form_for(resource, as: resource_name, url: authenticate_path) do |f|
6
+ %h1 Log In
7
+ = form_for(resource, as: resource_name, url: user_session_path) do |f|
8
8
  .field
9
9
  = f.label :email
10
10
  = f.email_field :email, autofocus: true, autocomplete: 'email'
@@ -18,8 +18,8 @@
18
18
  Remember Me
19
19
  %br
20
20
  .actions
21
- = f.submit 'Log in'
21
+ = f.submit 'Log In'
22
22
  - if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations'
23
23
  = link_to 'Forgot your password?', new_password_path(resource_name)
24
24
  - if flash.alert
25
- .error= flash.alert
25
+ .error= flash.alert
@@ -0,0 +1,14 @@
1
+ - body_classes << 'login_form'
2
+ .login-form-content
3
+ .visual
4
+ = image_tag('/assets/admin/default_safe_login.svg', alt: 'Web browser with padlock on top')
5
+ .login
6
+ %h1
7
+ = t('two_factor_authentication')
8
+ = form_with url: admin_two_factor_path, method: :post, local: true do |form|
9
+ .field
10
+ = form.label :otp_attempt, t('security_controller.enter_qr_code')
11
+ = form.text_field :otp_attempt, autofocus: true, maxlength: 6, size: 6
12
+ %br
13
+ .actions
14
+ = form.submit t('security_controller.verify_code')
@@ -15,9 +15,6 @@
15
15
  = f.label :email, t('email_address') , :class => 'optional'
16
16
  = f.text_field 'email', :class => 'textbox', :maxlength => 255
17
17
 
18
- - form.edit_password do
19
- = render 'password_fields', :f => f
20
-
21
18
  - form.edit_roles do
22
19
  - if current_user.admin?
23
20
  %fieldset.multi_option
@@ -8,16 +8,14 @@
8
8
  = f.hidden_field :reset_password_token
9
9
  .field
10
10
  = f.label :password, 'New password'
11
- %br/
12
11
  - if @minimum_password_length
13
12
  %em
14
13
  (#{@minimum_password_length} characters minimum)
15
- %br/
14
+
16
15
  = f.password_field :password, autofocus: true, autocomplete: 'new-password'
16
+ %br/
17
17
  .field
18
18
  = f.label :password_confirmation, 'Confirm new password'
19
- %br/
20
19
  = f.password_field :password_confirmation, autocomplete: 'new-password'
21
20
  .actions
22
21
  = f.submit 'Change my password'
23
- = render 'devise/shared/links'
@@ -0,0 +1,5 @@
1
+ ActiveRecord::Encryption.configure(
2
+ primary_key: ENV["ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY"],
3
+ deterministic_key: ENV["ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY"],
4
+ key_derivation_salt: ENV["ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT"]
5
+ )
@@ -11,6 +11,10 @@ require 'devise'
11
11
  # Use this hook to configure devise mailer, warden hooks and so forth.
12
12
  # Many of these configuration options can be set straight in your model.
13
13
  Devise.setup do |config|
14
+ config.warden do |manager|
15
+ manager.default_strategies(:scope => :user).unshift :two_factor_authenticatable
16
+ end
17
+
14
18
  # The secret key used by Devise. Devise uses this key to generate
15
19
  # random tokens. Changing this key will render invalid all existing
16
20
  # confirmation, reset password and unlock tokens in the database.
@@ -12,7 +12,6 @@ Rails.application.reloader.to_prepare do
12
12
  config.define 'admin.pagination.per_page', type: :integer, default: 50
13
13
  config.define 'site.title', default: 'Your site title', allow_blank: false
14
14
  config.define 'site.host', default: 'www.example.com', allow_blank: false
15
- config.define 'user.allow_password_reset?', default: true
16
15
  config.define 'session_timeout', default: 2.weeks
17
16
  require 'multi_site/scoped_validation'
18
17
  end
@@ -55,6 +55,7 @@ en:
55
55
  save_changes: 'Save Changes'
56
56
  cancel: 'Cancel'
57
57
  change: 'Change'
58
+ change_password: 'Change Password'
58
59
  clipped_extension:
59
60
  actions: Actions
60
61
  all: 'All'
@@ -155,8 +156,6 @@ en:
155
156
  site:
156
157
  title: "site title"
157
158
  host: "site domain"
158
- user:
159
- allow_password_reset?: "allow password reset"
160
159
  content: 'Content'
161
160
  content_type: 'Content&#8209;Type'
162
161
  content_editor: 'Content Editor'
@@ -182,6 +181,8 @@ en:
182
181
  description: 'Description'
183
182
  design: 'Design'
184
183
  designer: 'Designer'
184
+ disable: 'Disable'
185
+ disabled: 'Disabled'
185
186
  draft: 'Draft'
186
187
  edit: 'Edit'
187
188
  editor: 'Editor'
@@ -189,14 +190,17 @@ en:
189
190
  edit_layout: 'Edit Layout'
190
191
  edit_page: 'Edit Page'
191
192
  edit_preferences: 'Edit Preferences'
193
+ edit_security_settings: 'Edit Security Settings'
192
194
  edit_settings: 'Edit Settings'
193
195
  edit_user: 'Edit User'
194
196
  email_address: 'E-mail Address'
197
+ enabled: 'Enabled'
195
198
  extension: 'Extension'
196
199
  extensions: 'Extensions'
197
200
  filter: 'Filter'
198
201
  hidden: 'Hidden'
199
202
  hide: 'Hide'
203
+ invalid_email_or_password: 'Invalid e-mail address or password.'
200
204
  keywords: 'Keywords'
201
205
  language: 'Language'
202
206
  layout: 'Layout'
@@ -267,11 +271,27 @@ en:
267
271
  validation_errors: "Validation errors occurred while processing this form. Please take a moment to review the form and correct any input errors before continuing."
268
272
  reviewed: 'Reviewed'
269
273
  roles: 'Roles'
270
- saving_changes: Saving Changes
271
- saving_preferences: Saving preferences
272
- scheduled: "Scheduled"
274
+ saving_changes: 'Saving Changes'
275
+ saving_preferences: 'Saving Preferences'
276
+ scheduled: 'Scheduled'
273
277
  search: 'Search'
274
278
  search_tags: 'Search Tags:'
279
+ security: 'Security'
280
+ security_controller:
281
+ enter_qr_code: 'Enter 6-Digit Verification Code'
282
+ error_updating_password: 'There was an error updating your password.'
283
+ manual_key_instructions: 'Or manually enter this secret key:'
284
+ password_updated: 'Password updated. Please log in again.'
285
+ scan_qr_instructions: 'Scan this QR code in your Authenticator app:'
286
+ two_factor_enabled: 'Two-Factor Authentication Enabled'
287
+ two_factor_is_enabled: 'Two-Factor Authentication is currently enabled.'
288
+ two_factor_disable_confirm: 'Are you sure you want to disable Two-Factor Authentication?'
289
+ two_factor_disabled: 'Two-Factor Authentication Disabled'
290
+ two_factor_disabled_error: 'Error Disabling Two-Factor Authentication'
291
+ two_factor_invalid_code: 'Invalid 2FA Code'
292
+ qr_alt: 'Scan this with Google Authenticator or Authy'
293
+ verify_code: 'Verify Code'
294
+ verify_and_enable: 'Verify and Enable 2FA'
275
295
  select:
276
296
  default: '<default>'
277
297
  inherit: '<inherit>'
@@ -312,6 +332,10 @@ en:
312
332
  at: 'at'
313
333
  by: 'by'
314
334
  last_updated: 'Last Updated'
335
+ two_factor_authentication: 'Two-Factor Authentication'
336
+ two_factor_controller:
337
+ invalid_code: 'Invalid two-factor authentication code.'
338
+ session_expired: 'Your session has expired. Please log in again.'
315
339
  type: 'Type'
316
340
  units:
317
341
  KB: "KB"
data/config/routes.rb CHANGED
@@ -1,9 +1,8 @@
1
1
  TrustyCms::Application.routes.draw do
2
2
  root to: 'site#show_page'
3
- devise_for :users, module: :devise, skip: :registration
4
- as :user do
5
- post 'authenticate', to: 'devise/sessions#create', as: :authenticate
6
- end
3
+ devise_for :users,
4
+ controllers: { sessions: 'admin/sessions' },
5
+ skip: :registration
7
6
  post '/page-status/refresh' => 'page_status#refresh'
8
7
  get '/rad_social/mail' => 'social_mailer#social_mail_form', as: :rad_social_mail_form
9
8
  post '/rad_social/mail' => 'social_mailer#create_social_mail', as: :rad_create_social_mail
@@ -46,6 +45,11 @@ TrustyCms::Application.routes.draw do
46
45
 
47
46
  namespace :admin do
48
47
  resource :preferences
48
+ resource :two_factor, only: [:show, :create], controller: 'two_factor', path: 'two-factor'
49
+ resource :security, controller: 'security' do
50
+ post :verify_two_factor, on: :collection
51
+ post :disable_two_factor, on: :collection
52
+ end
49
53
  resource :configuration, controller: 'configuration'
50
54
  resources :extensions, only: :index
51
55
  resources :page_parts
@@ -0,0 +1,7 @@
1
+ class AddDeviseTwoFactorToAdmins < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :admins, :otp_secret, :string
4
+ add_column :admins, :consumed_timestep, :integer
5
+ add_column :admins, :otp_required_for_login, :boolean
6
+ end
7
+ end
@@ -150,6 +150,7 @@ module TrustyCms
150
150
  settings = nav_tab('Settings')
151
151
  settings << nav_item('General', '/admin/configuration')
152
152
  settings << nav_item('Personal', '/admin/preferences')
153
+ settings << nav_item('Security', '/admin/security')
153
154
  settings << nav_item('Users', '/admin/users')
154
155
  settings << nav_item('Extensions', '/admin/extensions')
155
156
  nav << settings
@@ -190,13 +191,17 @@ module TrustyCms
190
191
  OpenStruct.new.tap do |user|
191
192
  user.preferences = RegionSet.new do |preferences|
192
193
  preferences.main.concat %w{edit_header edit_form}
193
- preferences.form.concat %w{edit_first_name edit_last_name edit_email edit_password}
194
+ preferences.form.concat %w{edit_first_name edit_last_name edit_email}
194
195
  preferences.form_bottom.concat %w{edit_buttons}
195
196
  end
197
+ user.security = RegionSet.new do |security|
198
+ security.main.concat %w{edit_header edit_form two_factor}
199
+ security.form.concat %w{edit_password}
200
+ security.form_bottom.concat %w{edit_buttons}
201
+ end
196
202
  user.edit = RegionSet.new do |edit|
197
203
  edit.main.concat %w{edit_header edit_form}
198
- edit.form.concat %w{edit_first_name edit_last_name edit_email edit_password
199
- edit_roles edit_notes}
204
+ edit.form.concat %w{edit_first_name edit_last_name edit_email edit_roles edit_notes}
200
205
  edit.form_bottom.concat %w{edit_buttons edit_timestamp}
201
206
  end
202
207
  user.index = RegionSet.new do |index|
@@ -231,11 +236,11 @@ module TrustyCms
231
236
  OpenStruct.new.tap do |configuration|
232
237
  configuration.show = RegionSet.new do |show|
233
238
  show.user.concat %w{preferences}
234
- show.trusty_config.concat %w{site defaults users}
239
+ show.trusty_config.concat %w{site defaults}
235
240
  end
236
241
  configuration.edit = RegionSet.new do |edit|
237
242
  edit.main.concat %w{edit_header edit_form}
238
- edit.form.concat %w{edit_site edit_defaults edit_users}
243
+ edit.form.concat %w{edit_site edit_defaults}
239
244
  edit.form_bottom.concat %w{edit_buttons}
240
245
  end
241
246
  end
@@ -1,3 +1,3 @@
1
1
  module TrustyCms
2
- VERSION = '7.0.28'.freeze
2
+ VERSION = '7.0.29'.freeze
3
3
  end
data/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "jquery-treetable": "^3.2.0-1",
14
14
  "jquery-ui": "^1.13.2",
15
15
  "jquery-ujs": "^1.2.2",
16
- "jquery-validation": "^1.19.5",
16
+ "jquery-validation": "^1.20.0",
17
17
  "js-cookie": "^3.0.1",
18
18
  "tablesaw": "^3.1.2"
19
19
  },
@@ -13,7 +13,6 @@ Rails.application.reloader.to_prepare do
13
13
  config.define 'admin.pagination.per_page', :type => :integer, :default => 50
14
14
  config.define 'site.title', :default => "Your site title", :allow_blank => false
15
15
  config.define 'site.host', :default => "www.example.com", :allow_blank => false
16
- config.define 'user.allow_password_reset?', :default => true
17
16
  end
18
17
 
19
18
  TrustyCms::Application.config do |config|
data/trusty_cms.gemspec CHANGED
@@ -33,6 +33,7 @@ a general purpose content management system--not merely a blogging engine.'
33
33
  s.add_dependency 'ckeditor', '>= 4.2.2', '< 4.4.0'
34
34
  s.add_dependency 'delocalize', '>= 0.2', '< 2.0'
35
35
  s.add_dependency 'devise'
36
+ s.add_dependency 'devise-two-factor'
36
37
  s.add_dependency 'drb'
37
38
  s.add_dependency 'execjs', '~> 2.7'
38
39
  s.add_dependency 'haml', '>= 5.0', '< 6.0'
@@ -55,6 +56,7 @@ a general purpose content management system--not merely a blogging engine.'
55
56
  s.add_dependency 'rdoc', '>= 5.1', '< 7.0'
56
57
  s.add_dependency 'RedCloth', '4.3.3'
57
58
  s.add_dependency 'roadie-rails'
59
+ s.add_dependency 'rqrcode'
58
60
  s.add_dependency 'sass-rails'
59
61
  s.add_dependency 'stringex', '>= 2.7.1', '< 2.9.0'
60
62
  s.add_dependency 'tzinfo', '>= 1.2.3', '< 2.1.0'
data/yarn.lock CHANGED
@@ -1029,10 +1029,10 @@ jquery-ujs@^1.2.2:
1029
1029
  dependencies:
1030
1030
  jquery ">=1.8.0"
1031
1031
 
1032
- jquery-validation@^1.19.5:
1033
- version "1.19.5"
1034
- resolved "https://registry.yarnpkg.com/jquery-validation/-/jquery-validation-1.19.5.tgz#557495b7cad79716897057c4447ad3cd76fda811"
1035
- integrity sha512-X2SmnPq1mRiDecVYL8edWx+yTBZDyC8ohWXFhXdtqFHgU9Wd4KHkvcbCoIZ0JaSaumzS8s2gXSkP8F7ivg/8ZQ==
1032
+ jquery-validation@^1.20.0:
1033
+ version "1.20.0"
1034
+ resolved "https://registry.yarnpkg.com/jquery-validation/-/jquery-validation-1.20.0.tgz#dbff6d8fe61b07d4b6f844bf2f5405146556b991"
1035
+ integrity sha512-c8tg4ltIIP6L7l0bZ79sRzOJYquyjS48kQZ6iv8MJ2r0OYztxtkWYKTReZyU2/zVFYiINB29i0Z/IRNNuJQN1g==
1036
1036
 
1037
1037
  jquery@>=1.6:
1038
1038
  version "3.6.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trusty-cms
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.28
4
+ version: 7.0.29
5
5
  platform: ruby
6
6
  authors:
7
7
  - TrustyCms CMS dev team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-04-11 00:00:00.000000000 Z
11
+ date: 2025-05-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activestorage-validator
@@ -140,6 +140,20 @@ dependencies:
140
140
  - - ">="
141
141
  - !ruby/object:Gem::Version
142
142
  version: '0'
143
+ - !ruby/object:Gem::Dependency
144
+ name: devise-two-factor
145
+ requirement: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ type: :runtime
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
143
157
  - !ruby/object:Gem::Dependency
144
158
  name: drb
145
159
  requirement: !ruby/object:Gem::Requirement
@@ -472,6 +486,20 @@ dependencies:
472
486
  - - ">="
473
487
  - !ruby/object:Gem::Version
474
488
  version: '0'
489
+ - !ruby/object:Gem::Dependency
490
+ name: rqrcode
491
+ requirement: !ruby/object:Gem::Requirement
492
+ requirements:
493
+ - - ">="
494
+ - !ruby/object:Gem::Version
495
+ version: '0'
496
+ type: :runtime
497
+ prerelease: false
498
+ version_requirements: !ruby/object:Gem::Requirement
499
+ requirements:
500
+ - - ">="
501
+ - !ruby/object:Gem::Version
502
+ version: '0'
475
503
  - !ruby/object:Gem::Dependency
476
504
  name: sass-rails
477
505
  requirement: !ruby/object:Gem::Requirement
@@ -762,8 +790,11 @@ files:
762
790
  - app/controllers/admin/preferences_controller.rb
763
791
  - app/controllers/admin/references_controller.rb
764
792
  - app/controllers/admin/resource_controller.rb
793
+ - app/controllers/admin/security_controller.rb
794
+ - app/controllers/admin/sessions_controller.rb
765
795
  - app/controllers/admin/sites_controller.rb
766
796
  - app/controllers/admin/snippets_controller.rb
797
+ - app/controllers/admin/two_factor_controller.rb
767
798
  - app/controllers/admin/users_controller.rb
768
799
  - app/controllers/application_controller.rb
769
800
  - app/controllers/page_status_controller.rb
@@ -871,6 +902,8 @@ files:
871
902
  - app/views/admin/removed/_show_bucket_link.html.haml
872
903
  - app/views/admin/removed/_upload_to_page.html.haml
873
904
  - app/views/admin/removed/bucket/_iframe.html.haml
905
+ - app/views/admin/security/edit.html.haml
906
+ - app/views/admin/sessions/new.html.haml
874
907
  - app/views/admin/sites/_form.haml
875
908
  - app/views/admin/sites/edit.haml
876
909
  - app/views/admin/sites/index.haml
@@ -882,9 +915,9 @@ files:
882
915
  - app/views/admin/snippets/index.html.haml
883
916
  - app/views/admin/snippets/new.html.haml
884
917
  - app/views/admin/snippets/remove.html.haml
918
+ - app/views/admin/two_factor/show.html.haml
885
919
  - app/views/admin/users/_choose_site.html.haml
886
920
  - app/views/admin/users/_form.html.haml
887
- - app/views/admin/users/_password_fields.html.haml
888
921
  - app/views/admin/users/edit.html.haml
889
922
  - app/views/admin/users/index.html.haml
890
923
  - app/views/admin/users/new.html.haml
@@ -892,7 +925,6 @@ files:
892
925
  - app/views/admin/welcome/login.html.haml
893
926
  - app/views/devise/passwords/edit.html.haml
894
927
  - app/views/devise/passwords/new.html.haml
895
- - app/views/devise/sessions/new.html.haml
896
928
  - app/views/devise/shared/_links.html.haml
897
929
  - app/views/devise_mailer/reset_password_instructions.html.haml
898
930
  - app/views/layouts/application.html.haml
@@ -915,6 +947,7 @@ files:
915
947
  - config/environments/development.rb
916
948
  - config/environments/production.rb
917
949
  - config/environments/test.rb
950
+ - config/initializers/active_record_encryption.rb
918
951
  - config/initializers/active_record_extensions.rb
919
952
  - config/initializers/assets.rb
920
953
  - config/initializers/devise.rb
@@ -981,6 +1014,7 @@ files:
981
1014
  - db/migrate/20250102212417_create_versions.rb
982
1015
  - db/migrate/20250103191133_create_version_associations.rb
983
1016
  - db/migrate/20250103191134_add_transaction_id_column_to_versions.rb
1017
+ - db/migrate/20250502162215_add_devise_two_factor_to_admins.rb
984
1018
  - db/schema.rb
985
1019
  - lib/active_record_extensions/active_record_extensions.rb
986
1020
  - lib/annotatable.rb
@@ -1,18 +0,0 @@
1
- %fieldset#display_password{:style=> (@user.new_record? or !@user.valid?) ? 'display: none' : nil}
2
- %label= t('password')
3
- %span.value
4
- &bull;&bull;&bull;&bull;&bull;
5
- %a.button{:href=>'#', :onclick=>"$('#display_password').hide(); $('#change_password').show()"}= t('change')
6
- %fieldset#change_password{:style=> (!@user.new_record? && @user.valid?) ? 'display: none' : nil}
7
- %p
8
- = f.label :password, t('new_password')
9
- = f.password_field 'password', :value => '', :maxlength => 40, :autocomplete => 'new-password'
10
- %p
11
- = f.label :password_confirmation, t('password_confirmation')
12
- = f.password_field 'password_confirmation', :value => '', :maxlength => 40, :autocomplete => 'new-password'
13
- - unless @user.new_record?
14
- %span
15
- = t('or')
16
- %a{:href=>'#', :class=>'cancel-button', :onclick=>" $('#display_password').show(); $('#change_password').hide()"}= t('cancel', class: 'alt')
17
-
18
-