pages_core 3.12.7 → 3.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) 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/pages_controller.rb +1 -1
  12. data/app/controllers/admin/recovery_codes_controller.rb +32 -0
  13. data/app/controllers/admin/sessions_controller.rb +65 -0
  14. data/app/controllers/admin/users_controller.rb +3 -9
  15. data/app/controllers/concerns/pages_core/authentication.rb +12 -10
  16. data/app/controllers/concerns/pages_core/error_reporting.rb +9 -19
  17. data/app/controllers/concerns/pages_core/static_cache_controller.rb +13 -2
  18. data/app/controllers/pages_core/admin_controller.rb +1 -1
  19. data/app/controllers/pages_core/attachments_controller.rb +1 -1
  20. data/app/controllers/pages_core/frontend/pages_controller.rb +1 -1
  21. data/app/controllers/pages_core/frontend_controller.rb +1 -10
  22. data/app/formatters/pages_core/image_embedder.rb +3 -3
  23. data/app/helpers/admin/pages_helper.rb +2 -2
  24. data/app/helpers/pages_core/admin/admin_helper.rb +12 -1
  25. data/app/helpers/pages_core/admin/content_tabs_helper.rb +3 -3
  26. data/app/helpers/pages_core/admin/form_builder.rb +1 -1
  27. data/app/helpers/pages_core/admin/image_uploads_helper.rb +6 -6
  28. data/app/helpers/pages_core/admin/labelled_field_helper.rb +1 -1
  29. data/app/helpers/pages_core/admin/locales_helper.rb +1 -1
  30. data/app/helpers/pages_core/application_helper.rb +3 -3
  31. data/app/helpers/pages_core/head_tags_helper.rb +8 -9
  32. data/app/helpers/pages_core/images_helper.rb +7 -7
  33. data/app/helpers/pages_core/open_graph_tags_helper.rb +2 -2
  34. data/app/helpers/pages_core/page_path_helper.rb +2 -2
  35. data/app/javascript/index.ts +0 -2
  36. data/app/jobs/pages_core/autopublish_job.rb +2 -0
  37. data/app/mailers/admin_mailer.rb +2 -2
  38. data/app/models/concerns/pages_core/has_otp.rb +27 -0
  39. data/app/models/concerns/pages_core/has_roles.rb +2 -2
  40. data/app/models/concerns/pages_core/page_model/dated_page.rb +1 -1
  41. data/app/models/concerns/pages_core/page_model/searchable.rb +1 -1
  42. data/app/models/concerns/pages_core/page_model/templateable.rb +22 -0
  43. data/app/models/concerns/pages_core/searchable_document.rb +3 -3
  44. data/app/models/otp_secret.rb +101 -0
  45. data/app/models/page.rb +1 -1
  46. data/app/models/page_builder.rb +9 -9
  47. data/app/models/page_exporter.rb +1 -1
  48. data/app/models/page_image.rb +1 -1
  49. data/app/models/page_path.rb +3 -3
  50. data/app/models/search_document.rb +3 -3
  51. data/app/models/tag.rb +1 -1
  52. data/app/models/user.rb +15 -37
  53. data/app/policies/user_policy.rb +4 -0
  54. data/app/services/pages_core/create_user_service.rb +2 -2
  55. data/app/services/pages_core/destroy_invite_service.rb +2 -2
  56. data/app/services/pages_core/invite_service.rb +2 -2
  57. data/app/views/admin/account_recoveries/new.html.erb +22 -0
  58. data/app/views/admin/account_recoveries/show.html.erb +37 -0
  59. data/app/views/admin/invites/show.html.erb +1 -1
  60. data/app/views/admin/otp_secrets/create.html.erb +7 -0
  61. data/app/views/admin/otp_secrets/new.html.erb +60 -0
  62. data/app/views/admin/pages/_edit_content.html.erb +1 -1
  63. data/app/views/admin/pages/_form.html.erb +12 -0
  64. data/app/views/admin/recovery_codes/_codes.html.erb +14 -0
  65. data/app/views/admin/recovery_codes/create.html.erb +7 -0
  66. data/app/views/admin/recovery_codes/new.html.erb +11 -0
  67. data/app/views/admin/sessions/_otp_form.html.erb +13 -0
  68. data/app/views/admin/sessions/new.html.erb +33 -0
  69. data/app/views/admin/sessions/verify_otp.html.erb +19 -0
  70. data/app/views/admin/users/edit.html.erb +31 -1
  71. data/app/views/admin/users/new.html.erb +1 -1
  72. data/app/views/admin_mailer/account_recovery.text.erb +10 -0
  73. data/app/views/layouts/admin/_header.html.erb +1 -1
  74. data/app/views/layouts/admin/_toast.html.erb +12 -0
  75. data/app/views/layouts/admin.html.erb +1 -1
  76. data/config/locales/en.yml +11 -3
  77. data/config/routes.rb +11 -6
  78. data/db/migrate/20111219033112_create_pages_tables.rb +0 -14
  79. data/db/migrate/20240126160700_add_2fa_fields.rb +22 -0
  80. data/db/migrate/20240129201300_remove_password_reset_tokens.rb +13 -0
  81. data/lib/pages_core/cache_sweeper.rb +3 -3
  82. data/lib/pages_core/extensions/string_extensions.rb +1 -1
  83. data/lib/pages_core/templates/configuration.rb +1 -1
  84. data/lib/pages_core/templates/template_configuration.rb +1 -1
  85. data/lib/pages_core.rb +7 -2
  86. data/lib/rails/generators/pages_core/install/install_generator.rb +0 -15
  87. data/lib/rails/generators/pages_core/rspec/templates/page_templates_spec.rb +1 -1
  88. metadata +53 -56
  89. data/app/controllers/admin/password_resets_controller.rb +0 -85
  90. data/app/controllers/sessions_controller.rb +0 -27
  91. data/app/javascript/controllers/LoginController.ts +0 -32
  92. data/app/models/password_reset_token.rb +0 -34
  93. data/app/views/admin/password_resets/show.html.erb +0 -21
  94. data/app/views/admin/users/login.html.erb +0 -65
  95. data/app/views/admin_mailer/password_reset.text.erb +0 -11
  96. data/lib/rails/generators/pages_core/install/templates/active_job_initializer.rb +0 -3
  97. data/lib/rails/generators/pages_core/install/templates/delayed_job +0 -7
  98. data/lib/rails/generators/pages_core/install/templates/delayed_job_initializer.rb +0 -18
@@ -6,7 +6,7 @@ module PagesCore
6
6
  def locales_with_dir
7
7
  locales = PagesCore.config.locales || {}
8
8
  locales.each_with_object({}) do |(key, name), hash|
9
- hash[key] = { name: name, dir: locale_direction(key) }
9
+ hash[key] = { name:, dir: locale_direction(key) }
10
10
  end
11
11
  end
12
12
 
@@ -21,10 +21,10 @@ module PagesCore
21
21
  end
22
22
  end
23
23
 
24
- def unique_page(page_name, &block)
24
+ def unique_page(page_name, &)
25
25
  page = Page.where(unique_name: page_name).first
26
26
  if page && block_given?
27
- output = capture(page.localize(content_locale), &block)
27
+ output = capture(page.localize(content_locale), &)
28
28
  concat(output)
29
29
  end
30
30
  page&.localize(content_locale)
@@ -44,7 +44,7 @@ module PagesCore
44
44
 
45
45
  def page_link_path(locale, page)
46
46
  if page.redirects?
47
- page.redirect_path(locale: locale)
47
+ page.redirect_path(locale:)
48
48
  else
49
49
  page_path(locale, page)
50
50
  end
@@ -50,9 +50,9 @@ module PagesCore
50
50
  # <%= feed_tags %>
51
51
  # <% end %>
52
52
  #
53
- def head_tag(&block)
53
+ def head_tag(&)
54
54
  # The block output must be captured first
55
- block_output = block_given? ? capture(&block) : nil
55
+ block_output = block_given? ? capture(&) : nil
56
56
 
57
57
  tag.head { safe_join(head_tag_contents(block_output), "\n") }
58
58
  end
@@ -64,34 +64,33 @@ module PagesCore
64
64
  def rss_link_tag(title, href)
65
65
  tag.link(rel: "alternate",
66
66
  type: "application/rss+xml",
67
- title: title,
68
- href: href)
67
+ title:,
68
+ href:)
69
69
  end
70
70
 
71
71
  private
72
72
 
73
73
  def head_tag_contents(block_output)
74
74
  [tag.meta(charset: "utf-8"),
75
- tag.meta("http-equiv" => "X-UA-Compatible", "content" => "IE=edge"),
76
75
  tag.title(document_title),
77
76
  meta_description_tag,
78
77
  meta_keywords_tag,
79
78
  (tag.link(rel: "image_src", href: meta_image) if meta_image?),
80
79
  open_graph_tags,
81
- csrf_meta_tags,
82
- block_output]
80
+ (csrf_meta_tags unless static_cached?),
81
+ block_output].compact_blank
83
82
  end
84
83
 
85
84
  def meta_description_tag
86
85
  return unless meta_description?
87
86
 
88
- tag.meta(name: "description", content: meta_description)
87
+ tag.meta(name: "description", content: meta_description&.strip)
89
88
  end
90
89
 
91
90
  def meta_keywords_tag
92
91
  return unless meta_keywords?
93
92
 
94
- tag.meta(name: "keywords", content: meta_keywords)
93
+ tag.meta(name: "keywords", content: meta_keywords&.strip)
95
94
  end
96
95
  end
97
96
  end
@@ -57,12 +57,12 @@ module PagesCore
57
57
  def picture_tag(image, ratio: nil, sizes: "100vw")
58
58
  tag.picture do
59
59
  safe_join(
60
- [webp_source(image, ratio: ratio, sizes: sizes || "100vw"),
60
+ [webp_source(image, ratio:, sizes: sizes || "100vw"),
61
61
  dynamic_image_tag(image,
62
62
  size: image_size(1050, ratio),
63
63
  crop: (ratio ? true : false),
64
- sizes: sizes,
65
- srcset: srcset(image, ratio: ratio))]
64
+ sizes:,
65
+ srcset: srcset(image, ratio:))]
66
66
  )
67
67
  end
68
68
  end
@@ -106,11 +106,11 @@ module PagesCore
106
106
  size ||= default_image_size
107
107
  size = fit_ratio(size, ratio) if ratio
108
108
 
109
- dynamic_image_tag(image, size: size, crop: ratio && true, upscale: false)
109
+ dynamic_image_tag(image, size:, crop: ratio && true, upscale: false)
110
110
  end
111
111
 
112
112
  def image_link_to(content, href)
113
- tag.a(content, href: href)
113
+ tag.a(content, href:)
114
114
  end
115
115
 
116
116
  def image_size(width, ratio)
@@ -139,8 +139,8 @@ module PagesCore
139
139
  return unless webp_compatible?(image)
140
140
 
141
141
  tag.source(type: "image/webp",
142
- srcset: srcset(image, ratio: ratio, format: :webp),
143
- sizes: sizes)
142
+ srcset: srcset(image, ratio:, format: :webp),
143
+ sizes:)
144
144
  end
145
145
 
146
146
  def webp_compatible?(image)
@@ -13,7 +13,7 @@ module PagesCore
13
13
  properties
14
14
  .compact
15
15
  .map do |name, content|
16
- tag.meta(property: "og:#{name}", content: content)
16
+ tag.meta(property: "og:#{name}", content:)
17
17
  end,
18
18
  "\n"
19
19
  )
@@ -42,7 +42,7 @@ module PagesCore
42
42
  site_name: PagesCore.config(:site_name),
43
43
  title: default_open_graph_title,
44
44
  image: (meta_image if meta_image?),
45
- description: default_open_graph_description,
45
+ description: default_open_graph_description&.strip,
46
46
  url: request.url }
47
47
  end
48
48
  end
@@ -30,7 +30,7 @@ module PagesCore
30
30
  private
31
31
 
32
32
  def page_redirect_url(locale, page)
33
- redirect = page.redirect_path(locale: locale)
33
+ redirect = page.redirect_path(locale:)
34
34
  return redirect if redirect =~ %r{^https?://}
35
35
 
36
36
  base_page_url + redirect
@@ -54,7 +54,7 @@ module PagesCore
54
54
  ActiveSupport::Deprecation.warn(
55
55
  "Calling page_url without locale is deprecated"
56
56
  )
57
- [(opts[:locale] || content_locale), page_or_locale]
57
+ [opts[:locale] || content_locale, page_or_locale]
58
58
  end
59
59
 
60
60
  def paginated_section(opts)
@@ -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,6 +4,8 @@ module PagesCore
4
4
  class AutopublishJob < ApplicationJob
5
5
  queue_as :pages_core
6
6
 
7
+ retry_on StandardError, attempts: 10, wait: :polynomially_longer
8
+
7
9
  def perform
8
10
  Autopublisher.run!
9
11
  end
@@ -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
@@ -16,9 +16,9 @@ module PagesCore
16
16
  def role_names=(names)
17
17
  self.roles = names.map do |name|
18
18
  if role?(name)
19
- roles.find_by(name: name)
19
+ roles.find_by(name:)
20
20
  else
21
- roles.new(name: name)
21
+ roles.new(name:)
22
22
  end
23
23
  end
24
24
  end
@@ -90,7 +90,7 @@ module PagesCore
90
90
  def siblings_by_date
91
91
  siblings.reorder("starts_at ASC, pages.id DESC")
92
92
  .where
93
- .not(id: id)
93
+ .not(id:)
94
94
  end
95
95
  end
96
96
  end
@@ -8,7 +8,7 @@ module PagesCore
8
8
  def search_document_attributes
9
9
  super.merge(
10
10
  published: published?,
11
- name: name,
11
+ name:,
12
12
  description: try(&:meta_description?) ? meta_description : excerpt,
13
13
  # content: "",
14
14
  tags: tag_names.join(" ")
@@ -7,6 +7,8 @@ module PagesCore
7
7
 
8
8
  included do
9
9
  before_validation :ensure_template
10
+
11
+ delegate :enabled_blocks, to: :template_config
10
12
  end
11
13
 
12
14
  def template_config
@@ -27,8 +29,28 @@ module PagesCore
27
29
  template
28
30
  end
29
31
 
32
+ def unconfigured_blocks
33
+ blocks = (localizations.where(locale:).pluck(:name)
34
+ .map(&:to_sym) -
35
+ configured_blocks) &
36
+ PagesCore::Templates::TemplateConfiguration.all_blocks
37
+
38
+ if block_given?
39
+ blocks.each do |block_name|
40
+ yield block_name, template_config.block(block_name)
41
+ end
42
+ end
43
+
44
+ blocks
45
+ end
46
+
30
47
  private
31
48
 
49
+ def configured_blocks
50
+ enabled_blocks + %i[name path_segment meta_title meta_description
51
+ open_graph_title open_graph_description]
52
+ end
53
+
32
54
  def singularized_subtemplate
33
55
  singularized = ActiveSupport::Inflector.singularize(base_template)
34
56
  return if base_template == singularized
@@ -18,7 +18,7 @@ module PagesCore
18
18
 
19
19
  class << self
20
20
  def index_all!(scope)
21
- scope.all.find_each { |r| new(r).index! }
21
+ scope.find_each { |r| new(r).index! }
22
22
  end
23
23
  end
24
24
 
@@ -53,7 +53,7 @@ module PagesCore
53
53
  end
54
54
 
55
55
  def update_index(locale, attrs)
56
- record.search_documents.create_or_find_by!(locale: locale).update(attrs)
56
+ record.search_documents.create_or_find_by!(locale:).update(attrs)
57
57
  end
58
58
  end
59
59
 
@@ -61,7 +61,7 @@ module PagesCore
61
61
  return {} unless respond_to?(:localized_attributes)
62
62
 
63
63
  content = localized_attributes.keys.map { |a| localizer.get(a) }.join(" ")
64
- { content: content }
64
+ { content: }
65
65
  end
66
66
 
67
67
  def update_search_documents!
@@ -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/page.rb CHANGED
@@ -78,7 +78,7 @@ class Page < ApplicationRecord
78
78
 
79
79
  def move(parent:, position:)
80
80
  Page.transaction do
81
- update(parent: parent) unless self.parent == parent
81
+ update(parent:) unless self.parent == parent
82
82
  insert_at(position)
83
83
  end
84
84
  end
@@ -19,7 +19,7 @@ class PageBuilder
19
19
 
20
20
  class << self
21
21
  def build(user, locale: nil, parent: nil, &block)
22
- new(user, locale: locale, parent: parent)
22
+ new(user, locale:, parent:)
23
23
  .run(&block)
24
24
  end
25
25
  end
@@ -30,20 +30,20 @@ class PageBuilder
30
30
  @parent = parent
31
31
  end
32
32
 
33
- def page(name, options = {}, &block)
33
+ def page(name, options = {}, &)
34
34
  page = Page.create(
35
- { name: name }.merge(default_options).merge(options)
35
+ { name: }.merge(default_options).merge(options)
36
36
  )
37
37
  if block_given?
38
38
  self.class
39
- .new(user, locale: locale, parent: page)
40
- .run(&block)
39
+ .new(user, locale:, parent: page)
40
+ .run(&)
41
41
  end
42
42
  page
43
43
  end
44
44
 
45
- def run(&block)
46
- instance_eval(&block)
45
+ def run(&)
46
+ instance_eval(&)
47
47
  end
48
48
 
49
49
  private
@@ -51,9 +51,9 @@ class PageBuilder
51
51
  def default_options
52
52
  {
53
53
  author: user,
54
- parent: parent,
54
+ parent:,
55
55
  status: 2,
56
- locale: locale
56
+ locale:
57
57
  }
58
58
  end
59
59
  end
@@ -88,7 +88,7 @@ class PageExporter
88
88
  def text_page(page)
89
89
  PagesCore::Templates::TemplateConfiguration
90
90
  .all_blocks
91
- .select { |attr| page.send("#{attr}?".to_sym) }
91
+ .select { |attr| page.send(:"#{attr}?") }
92
92
  .map { |attr| ["-- #{attr}: --", page.send(attr).strip].join("\n\n") }
93
93
  .join("\n\n\n")
94
94
  end
@@ -15,7 +15,7 @@ class PageImage < ApplicationRecord
15
15
 
16
16
  class << self
17
17
  def cleanup!
18
- all.find_each do |page_image|
18
+ find_each do |page_image|
19
19
  page_image.destroy unless page_image.image
20
20
  end
21
21
  end
@@ -28,7 +28,7 @@ class PagePath < ApplicationRecord
28
28
  end
29
29
 
30
30
  def get(locale, path)
31
- find_by(locale: locale, path: path)
31
+ find_by(locale:, path:)
32
32
  end
33
33
 
34
34
  def associate(page, locale: nil, path: nil)
@@ -39,14 +39,14 @@ class PagePath < ApplicationRecord
39
39
  raise NoPathError unless path
40
40
 
41
41
  page_path = get_or_create(locale, path, page)
42
- page_path.update(page: page) unless page_path.page_id == page.id
42
+ page_path.update(page:) unless page_path.page_id == page.id
43
43
  page_path
44
44
  end
45
45
 
46
46
  private
47
47
 
48
48
  def get_or_create(locale, path, page)
49
- get(locale, path) || create(locale: locale, path: path, page: page)
49
+ get(locale, path) || create(locale:, path:, page:)
50
50
  end
51
51
  end
52
52
  end
@@ -15,12 +15,12 @@ class SearchDocument < ApplicationRecord
15
15
  pg_search_scope :full_text_search_scope, lambda { |query, dictionary|
16
16
  { against: %i[name description content tags],
17
17
  using: { tsearch: { prefix: true,
18
- dictionary: dictionary,
18
+ dictionary:,
19
19
  tsvector_column: "tsv" },
20
20
  trigram: { only: %i[name] } },
21
21
  ignoring: :accents,
22
22
  order_within_rank: "search_documents.record_updated_at DESC",
23
- query: query }
23
+ query: }
24
24
  }
25
25
 
26
26
  class << self
@@ -31,7 +31,7 @@ class SearchDocument < ApplicationRecord
31
31
 
32
32
  def search(query, locale: nil)
33
33
  locale ||= I18n.locale
34
- where(locale: locale)
34
+ where(locale:)
35
35
  .includes(:searchable)
36
36
  .full_text_search_scope(query, search_configuration(locale))
37
37
  end
data/app/models/tag.rb CHANGED
@@ -57,7 +57,7 @@ class Tag < ApplicationRecord
57
57
  end
58
58
 
59
59
  def on(taggable)
60
- taggings.create(taggable: taggable)
60
+ taggings.create(taggable:)
61
61
  end
62
62
 
63
63
  def ==(other)
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