pages_core 3.13.0 → 3.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/app/assets/builds/pages_core/admin-dist.js +19 -8
  4. data/app/assets/builds/pages_core/admin-dist.js.map +4 -4
  5. data/app/assets/builds/pages_core/admin.css +704 -388
  6. data/app/assets/fonts/Inter-Black.woff2 +0 -0
  7. data/app/assets/fonts/Inter-BlackItalic.woff2 +0 -0
  8. data/app/assets/fonts/Inter-Bold.woff2 +0 -0
  9. data/app/assets/fonts/Inter-BoldItalic.woff2 +0 -0
  10. data/app/assets/fonts/Inter-ExtraBold.woff2 +0 -0
  11. data/app/assets/fonts/Inter-ExtraBoldItalic.woff2 +0 -0
  12. data/app/assets/fonts/Inter-ExtraLight.woff2 +0 -0
  13. data/app/assets/fonts/Inter-ExtraLightItalic.woff2 +0 -0
  14. data/app/assets/fonts/Inter-Italic.woff2 +0 -0
  15. data/app/assets/fonts/Inter-Light.woff2 +0 -0
  16. data/app/assets/fonts/Inter-LightItalic.woff2 +0 -0
  17. data/app/assets/fonts/Inter-Medium.woff2 +0 -0
  18. data/app/assets/fonts/Inter-MediumItalic.woff2 +0 -0
  19. data/app/assets/fonts/Inter-Regular.woff2 +0 -0
  20. data/app/assets/fonts/Inter-SemiBold.woff2 +0 -0
  21. data/app/assets/fonts/Inter-SemiBoldItalic.woff2 +0 -0
  22. data/app/assets/fonts/Inter-Thin.woff2 +0 -0
  23. data/app/assets/fonts/Inter-ThinItalic.woff2 +0 -0
  24. data/app/assets/fonts/InterDisplay-Black.woff2 +0 -0
  25. data/app/assets/fonts/InterDisplay-BlackItalic.woff2 +0 -0
  26. data/app/assets/fonts/InterDisplay-Bold.woff2 +0 -0
  27. data/app/assets/fonts/InterDisplay-BoldItalic.woff2 +0 -0
  28. data/app/assets/fonts/InterDisplay-ExtraBold.woff2 +0 -0
  29. data/app/assets/fonts/InterDisplay-ExtraBoldItalic.woff2 +0 -0
  30. data/app/assets/fonts/InterDisplay-ExtraLight.woff2 +0 -0
  31. data/app/assets/fonts/InterDisplay-ExtraLightItalic.woff2 +0 -0
  32. data/app/assets/fonts/InterDisplay-Italic.woff2 +0 -0
  33. data/app/assets/fonts/InterDisplay-Light.woff2 +0 -0
  34. data/app/assets/fonts/InterDisplay-LightItalic.woff2 +0 -0
  35. data/app/assets/fonts/InterDisplay-Medium.woff2 +0 -0
  36. data/app/assets/fonts/InterDisplay-MediumItalic.woff2 +0 -0
  37. data/app/assets/fonts/InterDisplay-Regular.woff2 +0 -0
  38. data/app/assets/fonts/InterDisplay-SemiBold.woff2 +0 -0
  39. data/app/assets/fonts/InterDisplay-SemiBoldItalic.woff2 +0 -0
  40. data/app/assets/fonts/InterDisplay-Thin.woff2 +0 -0
  41. data/app/assets/fonts/InterDisplay-ThinItalic.woff2 +0 -0
  42. data/app/assets/fonts/InterVariable-Italic.woff2 +0 -0
  43. data/app/assets/fonts/InterVariable.woff2 +0 -0
  44. data/app/assets/stylesheets/pages_core/admin/components/archive.css +1 -1
  45. data/app/assets/stylesheets/pages_core/admin/components/attachments.css +22 -34
  46. data/app/assets/stylesheets/pages_core/admin/components/base.css +1 -68
  47. data/app/assets/stylesheets/pages_core/admin/components/forms.css +107 -48
  48. data/app/assets/stylesheets/pages_core/admin/components/header.css +56 -58
  49. data/app/assets/stylesheets/pages_core/admin/components/image_editor.css +35 -24
  50. data/app/assets/stylesheets/pages_core/admin/components/image_grid.css +28 -27
  51. data/app/assets/stylesheets/pages_core/admin/components/image_uploader.css +5 -5
  52. data/app/assets/stylesheets/pages_core/admin/components/layout.css +7 -1
  53. data/app/assets/stylesheets/pages_core/admin/components/list_table.css +24 -15
  54. data/app/assets/stylesheets/pages_core/admin/components/page_tree.css +63 -104
  55. data/app/assets/stylesheets/pages_core/admin/components/pagination.css +12 -13
  56. data/app/assets/stylesheets/pages_core/admin/components/search.css +1 -16
  57. data/app/assets/stylesheets/pages_core/admin/components/sidebar.css +5 -11
  58. data/app/assets/stylesheets/pages_core/admin/components/tag_editor.css +22 -36
  59. data/app/assets/stylesheets/pages_core/admin/components/toast.css +1 -2
  60. data/app/assets/stylesheets/pages_core/admin/components/toolbar.css +10 -10
  61. data/app/assets/stylesheets/pages_core/admin/components/totp.css +26 -0
  62. data/app/assets/stylesheets/pages_core/admin/controllers/pages.css +37 -51
  63. data/app/assets/stylesheets/pages_core/admin/global/fonts.css +271 -0
  64. data/app/assets/stylesheets/pages_core/admin/global/typography.css +109 -0
  65. data/app/assets/stylesheets/pages_core/admin/vars.css +1 -3
  66. data/app/assets/stylesheets/pages_core/admin.postcss.css +1 -0
  67. data/app/controllers/admin/account_recoveries_controller.rb +87 -0
  68. data/app/controllers/admin/invites_controller.rb +3 -2
  69. data/app/controllers/admin/otp_secrets_controller.rb +45 -0
  70. data/app/controllers/admin/pages_controller.rb +22 -42
  71. data/app/controllers/admin/recovery_codes_controller.rb +32 -0
  72. data/app/controllers/admin/sessions_controller.rb +65 -0
  73. data/app/controllers/admin/users_controller.rb +2 -8
  74. data/app/controllers/concerns/pages_core/authentication.rb +12 -10
  75. data/app/controllers/concerns/pages_core/error_reporting.rb +1 -1
  76. data/app/controllers/concerns/pages_core/page_parameters.rb +29 -0
  77. data/app/controllers/concerns/pages_core/policies_helper.rb +1 -1
  78. data/app/controllers/concerns/pages_core/preview_pages_controller.rb +20 -20
  79. data/app/controllers/pages_core/admin_controller.rb +1 -3
  80. data/app/controllers/pages_core/frontend/pages_controller.rb +2 -6
  81. data/app/formatters/pages_core/html_formatter.rb +2 -4
  82. data/app/helpers/admin/menu_helper.rb +5 -4
  83. data/app/helpers/admin/pages_helper.rb +1 -21
  84. data/app/helpers/pages_core/admin/admin_helper.rb +13 -3
  85. data/app/helpers/pages_core/admin/content_tabs_helper.rb +1 -2
  86. data/app/helpers/pages_core/admin/labelled_field_helper.rb +1 -1
  87. data/app/helpers/pages_core/frontend_helper.rb +1 -1
  88. data/app/helpers/pages_core/images_helper.rb +10 -8
  89. data/app/helpers/pages_core/labelled_form_builder.rb +2 -7
  90. data/app/helpers/pages_core/page_path_helper.rb +1 -1
  91. data/app/javascript/components/Attachments/Attachment.tsx +20 -18
  92. data/app/javascript/components/Attachments/AttachmentEditor.tsx +11 -9
  93. data/app/javascript/components/{Attachments.jsx → Attachments/List.tsx} +58 -63
  94. data/app/javascript/components/Attachments/useAttachments.ts +15 -0
  95. data/app/javascript/components/Attachments.tsx +14 -0
  96. data/app/javascript/components/DateRangeSelect.tsx +105 -0
  97. data/app/javascript/components/DateTimeSelect.tsx +136 -0
  98. data/app/javascript/components/EditableImage.tsx +11 -9
  99. data/app/javascript/components/FileUploadButton.tsx +7 -7
  100. data/app/javascript/components/ImageCropper/FocalPoint.tsx +9 -12
  101. data/app/javascript/components/ImageCropper/Image.tsx +10 -8
  102. data/app/javascript/components/ImageCropper/Toolbar.tsx +11 -12
  103. data/app/javascript/components/ImageCropper/useCrop.ts +24 -53
  104. data/app/javascript/components/ImageCropper.tsx +10 -15
  105. data/app/javascript/components/ImageEditor/Form.tsx +12 -8
  106. data/app/javascript/components/ImageEditor.tsx +12 -7
  107. data/app/javascript/components/ImageGrid/DragElement.tsx +9 -12
  108. data/app/javascript/components/{ImageGrid.jsx → ImageGrid/Grid.tsx} +62 -71
  109. data/app/javascript/components/ImageGrid/GridImage.tsx +22 -23
  110. data/app/javascript/components/ImageGrid/Placeholder.tsx +2 -2
  111. data/app/javascript/components/ImageGrid/useImageGrid.ts +26 -0
  112. data/app/javascript/components/ImageGrid.tsx +15 -0
  113. data/app/javascript/components/ImageUploader.tsx +35 -22
  114. data/app/javascript/components/LabelledField.tsx +34 -0
  115. data/app/javascript/components/Modal.tsx +2 -2
  116. data/app/javascript/components/PageForm/Block.tsx +81 -0
  117. data/app/javascript/components/PageForm/Content.tsx +54 -0
  118. data/app/javascript/components/PageForm/Dates.tsx +66 -0
  119. data/app/javascript/components/PageForm/Files.tsx +28 -0
  120. data/app/javascript/components/PageForm/Form.tsx +41 -0
  121. data/app/javascript/components/PageForm/Images.tsx +28 -0
  122. data/app/javascript/components/PageForm/LocaleLinks.tsx +36 -0
  123. data/app/javascript/components/PageForm/Metadata.tsx +67 -0
  124. data/app/javascript/components/PageForm/Options.tsx +180 -0
  125. data/app/javascript/components/PageForm/PageDescription.tsx +48 -0
  126. data/app/javascript/components/PageForm/PathSegment.tsx +65 -0
  127. data/app/javascript/components/PageForm/TabPanel.tsx +21 -0
  128. data/app/javascript/components/PageForm/Tabs.tsx +33 -0
  129. data/app/javascript/components/PageForm/UnconfiguredContent.tsx +42 -0
  130. data/app/javascript/components/PageForm/pageParams.ts +95 -0
  131. data/app/javascript/components/PageForm/preview.ts +23 -0
  132. data/app/javascript/components/PageForm/usePage.ts +169 -0
  133. data/app/javascript/components/PageForm/useTabs.ts +46 -0
  134. data/app/javascript/components/PageForm.tsx +163 -0
  135. data/app/javascript/components/PageImages.tsx +7 -9
  136. data/app/javascript/components/PageTree/Draggable.tsx +40 -39
  137. data/app/javascript/components/PageTree/Node.tsx +62 -56
  138. data/app/javascript/components/PageTree/PageName.tsx +28 -0
  139. data/app/javascript/components/PageTree.tsx +65 -53
  140. data/app/javascript/components/{RichTextArea.jsx → RichTextArea.tsx} +98 -79
  141. data/app/javascript/components/RichTextToolbarButton.tsx +4 -6
  142. data/app/javascript/components/TagEditor/AddTagForm.tsx +19 -12
  143. data/app/javascript/components/TagEditor/Editor.tsx +32 -0
  144. data/app/javascript/components/TagEditor/Tag.tsx +6 -4
  145. data/app/javascript/components/TagEditor/useTags.ts +58 -0
  146. data/app/javascript/components/TagEditor.tsx +8 -58
  147. data/app/javascript/components/Toast.tsx +3 -3
  148. data/app/javascript/components/drag/draggedOrder.ts +22 -14
  149. data/app/javascript/components/drag/useDragCollection.ts +35 -30
  150. data/app/javascript/components/drag/useDragUploader.ts +32 -21
  151. data/app/javascript/components/drag/useDraggable.ts +7 -6
  152. data/app/javascript/components/drag.ts +0 -1
  153. data/app/javascript/components.ts +1 -3
  154. data/app/javascript/features/RichText.tsx +2 -3
  155. data/app/javascript/features/contentTabs.ts +79 -0
  156. data/app/javascript/index.ts +5 -14
  157. data/app/javascript/lib/Tree.ts +31 -45
  158. data/app/javascript/lib/request.ts +11 -11
  159. data/app/javascript/stores/useToastStore.ts +1 -1
  160. data/app/javascript/types/Attachments.ts +29 -0
  161. data/app/javascript/types/Crop.ts +36 -0
  162. data/app/javascript/types/Drag.ts +34 -0
  163. data/app/javascript/types/Images.ts +47 -0
  164. data/app/javascript/types/PageEditor.ts +26 -0
  165. data/app/javascript/types/Pages.ts +75 -0
  166. data/app/javascript/types/Tags.ts +9 -0
  167. data/app/javascript/types/Template.ts +24 -0
  168. data/app/javascript/types/Trees.ts +19 -0
  169. data/app/javascript/types.ts +2 -25
  170. data/app/mailers/admin_mailer.rb +2 -2
  171. data/app/models/attachment.rb +1 -1
  172. data/app/models/concerns/pages_core/authenticable_user.rb +63 -0
  173. data/app/models/concerns/pages_core/emailable.rb +16 -0
  174. data/app/models/concerns/pages_core/page_model/templateable.rb +2 -16
  175. data/app/models/invite.rb +2 -6
  176. data/app/models/otp_secret.rb +101 -0
  177. data/app/models/page.rb +0 -3
  178. data/app/models/user.rb +2 -68
  179. data/app/policies/page_policy.rb +6 -2
  180. data/app/policies/user_policy.rb +4 -0
  181. data/app/resources/admin/page_resource.rb +95 -0
  182. data/app/resources/admin/page_tree_resource.rb +27 -0
  183. data/app/resources/admin/template_configuration_resource.rb +50 -0
  184. data/app/views/admin/account_recoveries/new.html.erb +22 -0
  185. data/app/views/admin/account_recoveries/show.html.erb +37 -0
  186. data/app/views/admin/invites/show.html.erb +1 -1
  187. data/app/views/admin/news/_sidebar.html.erb +2 -4
  188. data/app/views/admin/news/index.html.erb +0 -1
  189. data/app/views/admin/otp_secrets/create.html.erb +7 -0
  190. data/app/views/admin/otp_secrets/new.html.erb +60 -0
  191. data/app/views/admin/pages/_form.html.erb +10 -30
  192. data/app/views/admin/pages/_search_bar.html.erb +1 -1
  193. data/app/views/admin/pages/edit.html.erb +1 -57
  194. data/app/views/admin/pages/index.html.erb +1 -1
  195. data/app/views/admin/pages/new.html.erb +1 -44
  196. data/app/views/admin/recovery_codes/_codes.html.erb +14 -0
  197. data/app/views/admin/recovery_codes/create.html.erb +7 -0
  198. data/app/views/admin/recovery_codes/new.html.erb +11 -0
  199. data/app/views/admin/sessions/_otp_form.html.erb +13 -0
  200. data/app/views/admin/sessions/new.html.erb +31 -0
  201. data/app/views/admin/sessions/verify_otp.html.erb +19 -0
  202. data/app/views/admin/users/_access_control.html.erb +5 -1
  203. data/app/views/admin/users/_list.html.erb +12 -7
  204. data/app/views/admin/users/edit.html.erb +31 -1
  205. data/app/views/admin/users/new.html.erb +1 -1
  206. data/app/views/admin_mailer/account_recovery.text.erb +10 -0
  207. data/app/views/layouts/admin/_header.html.erb +3 -5
  208. data/app/views/layouts/admin/_page_header.html.erb +1 -2
  209. data/app/views/layouts/admin/_toast.html.erb +12 -0
  210. data/app/views/layouts/admin.html.erb +2 -2
  211. data/config/locales/en.yml +11 -7
  212. data/config/routes.rb +13 -12
  213. data/db/migrate/20240126160700_add_2fa_fields.rb +26 -0
  214. data/db/migrate/20240129201300_remove_password_reset_tokens.rb +13 -0
  215. data/db/migrate/20240131140700_change_email_to_citext.rb +18 -0
  216. data/db/migrate/20240201160700_remove_persistent_data.rb +7 -0
  217. data/db/migrate/20240508145300_remove_categories.rb +21 -0
  218. data/lib/pages_core/configuration/base.rb +2 -2
  219. data/lib/pages_core/templates/configuration.rb +1 -1
  220. data/lib/pages_core/templates/configuration_proxy.rb +2 -2
  221. data/lib/pages_core/templates/template_configuration.rb +11 -1
  222. data/lib/pages_core/templates.rb +6 -4
  223. data/lib/pages_core/version.rb +1 -1
  224. data/lib/pages_core.rb +6 -0
  225. data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/gridOverlay.ts +6 -7
  226. data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/responsiveEmbeds.ts +17 -12
  227. data/lib/rails/generators/pages_core/rspec/rspec_generator.rb +0 -2
  228. data/lib/rails/generators/pages_core/rspec/templates/rails_helper.rb +3 -4
  229. metadata +143 -35
  230. data/app/assets/stylesheets/pages_core/admin/components/login.css +0 -33
  231. data/app/controllers/admin/categories_controller.rb +0 -56
  232. data/app/controllers/admin/password_resets_controller.rb +0 -85
  233. data/app/controllers/concerns/pages_core/admin/persistent_params.rb +0 -75
  234. data/app/controllers/sessions_controller.rb +0 -27
  235. data/app/helpers/pages_core/admin/page_blocks_helper.rb +0 -66
  236. data/app/helpers/pages_core/admin/page_json_helper.rb +0 -23
  237. data/app/javascript/components/DateRangeSelect.jsx +0 -225
  238. data/app/javascript/components/PageDates.jsx +0 -73
  239. data/app/javascript/components/PageFiles.jsx +0 -25
  240. data/app/javascript/components/PageTree/types.ts +0 -15
  241. data/app/javascript/components/drag/types.ts +0 -28
  242. data/app/javascript/controllers/EditPageController.ts +0 -22
  243. data/app/javascript/controllers/LoginController.ts +0 -32
  244. data/app/javascript/controllers/MainController.ts +0 -74
  245. data/app/javascript/controllers/PageOptionsController.js +0 -67
  246. data/app/models/category.rb +0 -22
  247. data/app/models/page_category.rb +0 -6
  248. data/app/models/password_reset_token.rb +0 -34
  249. data/app/views/admin/pages/_edit_content.html.erb +0 -19
  250. data/app/views/admin/pages/_edit_files.html.erb +0 -4
  251. data/app/views/admin/pages/_edit_images.html.erb +0 -4
  252. data/app/views/admin/pages/_edit_metadata.html.erb +0 -35
  253. data/app/views/admin/pages/_edit_options.html.erb +0 -91
  254. data/app/views/admin/password_resets/show.html.erb +0 -21
  255. data/app/views/admin/users/login.html.erb +0 -65
  256. data/app/views/admin_mailer/password_reset.text.erb +0 -11
  257. data/lib/rails/generators/pages_core/rspec/templates/mailer_macros.rb +0 -11
@@ -29,21 +29,6 @@ module PagesCore
29
29
  template
30
30
  end
31
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
-
47
32
  private
48
33
 
49
34
  def configured_blocks
@@ -69,8 +54,9 @@ module PagesCore
69
54
  end
70
55
 
71
56
  def base_template
57
+ reject_words = %w[index list archive liste arkiv]
72
58
  template.split("_")
73
- .reject { |w| %w[index list archive liste arkiv].include?(w) }
59
+ .reject { |w| reject_words.include?(w) }
74
60
  .join(" ")
75
61
  end
76
62
 
data/app/models/invite.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Invite < ApplicationRecord
4
+ include PagesCore::Emailable
4
5
  include PagesCore::HasRoles
5
6
 
6
7
  belongs_to :user
@@ -8,11 +9,6 @@ class Invite < ApplicationRecord
8
9
 
9
10
  before_validation :ensure_token
10
11
 
11
- validates :email,
12
- presence: true,
13
- format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i },
14
- uniqueness: { case_sensitive: false }
15
-
16
12
  validates :token, presence: true
17
13
 
18
14
  validate :user_already_exists
@@ -24,7 +20,7 @@ class Invite < ApplicationRecord
24
20
  end
25
21
 
26
22
  def user_already_exists
27
- return unless User.find_by_email(email)
23
+ return unless User.find_by(email:)
28
24
 
29
25
  errors.add(:email, :taken)
30
26
  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
+ Array.new(10) { 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 /^[\d]{6}$/.match?(code)
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, issuer: PagesCore.config.site_name)
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).symbolize_keys
97
+ raise "Wrong user" unless payload[:user_id] == user.id
98
+
99
+ payload[:secret]
100
+ end
101
+ end
data/app/models/page.rb CHANGED
@@ -25,9 +25,6 @@ class Page < ApplicationRecord
25
25
  optional: true,
26
26
  inverse_of: :pages
27
27
 
28
- has_many :page_categories, dependent: :destroy
29
- has_many :categories, through: :page_categories
30
-
31
28
  validates(:unique_name,
32
29
  format: { with: /\A[\w\d_-]+\z/,
33
30
  allow_blank: true },
data/app/models/user.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class User < ApplicationRecord
4
+ include PagesCore::AuthenticableUser
5
+ include PagesCore::Emailable
4
6
  include PagesCore::HasRoles
5
7
 
6
- attr_accessor :password, :confirm_password
7
-
8
8
  belongs_to(:creator,
9
9
  class_name: "User",
10
10
  foreign_key: "created_by",
@@ -16,54 +16,18 @@ class User < ApplicationRecord
16
16
  dependent: :nullify,
17
17
  inverse_of: :creator)
18
18
  has_many :pages, dependent: :nullify
19
- has_many :password_reset_tokens, dependent: :destroy
20
19
  has_many :roles, dependent: :destroy
21
20
  has_many :invites, dependent: :destroy
22
21
  belongs_to_image :image, foreign_key: :image_id, optional: true
23
22
 
24
- serialize :persistent_data
25
-
26
23
  validates :name, presence: true
27
24
 
28
- validates :email,
29
- presence: true,
30
- format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i },
31
- uniqueness: { case_sensitive: false }
32
-
33
- validates :password, presence: true, on: :create
34
- validates :password, length: { minimum: 8 }, allow_blank: true
35
-
36
- validate :confirm_password_must_match
37
-
38
- before_validation :hash_password
39
25
  before_create :ensure_first_user_has_all_roles
40
26
 
41
27
  scope :by_name, -> { order("name ASC") }
42
28
  scope :activated, -> { by_name.includes(:roles).where(activated: true) }
43
29
  scope :deactivated, -> { by_name.includes(:roles).where(activated: false) }
44
30
 
45
- class << self
46
- def authenticate(email, password:)
47
- user = User.find_by_email(email)
48
- user if user.try { |u| u.authenticate!(password) }
49
- end
50
-
51
- def find_by_email(str)
52
- find_by("LOWER(email) = ?", str.to_s.downcase)
53
- end
54
- end
55
-
56
- def authenticate!(password)
57
- return false unless can_login? && valid_password?(password)
58
-
59
- rehash_password!(password) if password_needs_rehash?
60
- true
61
- end
62
-
63
- def can_login?
64
- activated?
65
- end
66
-
67
31
  def mark_active!
68
32
  return if last_login_at && last_login_at > 10.minutes.ago
69
33
 
@@ -84,16 +48,6 @@ class User < ApplicationRecord
84
48
 
85
49
  private
86
50
 
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
51
  def ensure_first_user_has_all_roles
98
52
  return if User.any?
99
53
 
@@ -102,24 +56,4 @@ class User < ApplicationRecord
102
56
  roles.new(name: r.name) unless role?(r.name)
103
57
  end
104
58
  end
105
-
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
113
-
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
124
- end
125
59
  end
@@ -18,7 +18,7 @@ class PagePolicy < Policy
18
18
  end
19
19
 
20
20
  def new?
21
- user.role?(:pages)
21
+ user&.role?(:pages)
22
22
  end
23
23
 
24
24
  def show?
@@ -26,7 +26,11 @@ class PagePolicy < Policy
26
26
  end
27
27
 
28
28
  def edit?
29
- user.role?(:pages)
29
+ user&.role?(:pages)
30
+ end
31
+
32
+ def edit2?
33
+ edit?
30
34
  end
31
35
 
32
36
  def move?
@@ -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
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Admin
4
+ class PageResource
5
+ include Alba::Resource
6
+ include Rails.application.routes.url_helpers
7
+ include PagesCore::PagePathHelper
8
+ include DynamicImage::Helper
9
+
10
+ attributes :id, :starts_at, :ends_at, :all_day, :status, :published_at,
11
+ :pinned, :template, :unique_name, :feed_enabled, :news_page,
12
+ :parent_page_id, :user_id, :redirect_to
13
+
14
+ has_many :page_images, resource: Admin::PageImageResource
15
+ has_many :page_files, resource: Admin::PageFileResource
16
+
17
+ attribute :blocks do
18
+ PagesCore::Templates::TemplateConfiguration.all_blocks
19
+ .index_with do |attr|
20
+ if object.template_config.block(attr)[:localized]
21
+ localized_attribute(object, attr)
22
+ else
23
+ object.send(attr)
24
+ end
25
+ end
26
+ end
27
+
28
+ attribute :errors do
29
+ object.errors.map do |e|
30
+ { attribute: e.attribute,
31
+ message: e.message }
32
+ end
33
+ end
34
+
35
+ attribute :urls do
36
+ if object.id?
37
+ localized_objects.filter(&:name?).each_with_object({}) do |p, obj|
38
+ obj[p.locale] = page_path(p.locale, p)
39
+ obj
40
+ end
41
+ else
42
+ {}
43
+ end
44
+ end
45
+
46
+ attribute :enabled_tags do
47
+ object.tags.map(&:name)
48
+ end
49
+
50
+ attribute :tags_and_suggestions do
51
+ Tag.tags_and_suggestions_for(object, limit: 20)
52
+ .map(&:name)
53
+ end
54
+
55
+ attribute :meta_image do
56
+ image_uploader(object.meta_image)
57
+ end
58
+
59
+ attribute :path_segment do
60
+ localized_attribute(object, :path_segment)
61
+ end
62
+
63
+ attribute :ancestors do
64
+ object.ancestors.map do |p|
65
+ { id: p.id,
66
+ name: localized_attribute(p, :name),
67
+ path_segment: localized_attribute(p, :path_segment) }
68
+ end
69
+ end
70
+
71
+ attribute :permissions do
72
+ [(:edit if Policy.for(params[:user], object).edit?),
73
+ (:create if Policy.for(params[:user], object).edit?)].compact
74
+ end
75
+
76
+ private
77
+
78
+ def image_uploader(image)
79
+ return { src: nil, image: nil } unless image
80
+
81
+ { src: dynamic_image_path(image, size: "500x"),
82
+ image: ::Admin::ImageResource.new(image).to_hash }
83
+ end
84
+
85
+ def localized_objects
86
+ object.locales.map { |l| object.localize(l) }
87
+ end
88
+
89
+ def localized_attribute(record, attr)
90
+ record.locales.index_with do |locale|
91
+ record.localize(locale).send(attr)
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Admin
4
+ class PageTreeResource
5
+ include Alba::Resource
6
+
7
+ attributes :id, :parent_page_id, :status, :news_page, :pinned,
8
+ :published_at
9
+
10
+ attribute :blocks do
11
+ { name: localized_attribute(object, :name) }
12
+ end
13
+
14
+ attribute :permissions do
15
+ [(:edit if Policy.for(params[:user], object).edit?),
16
+ (:create if Policy.for(params[:user], object).edit?)].compact
17
+ end
18
+
19
+ private
20
+
21
+ def localized_attribute(record, attr)
22
+ record.locales.index_with do |locale|
23
+ record.localize(locale).send(attr)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Admin
4
+ class TemplateConfigurationResource
5
+ include Alba::Resource
6
+
7
+ attributes :name, :template_name
8
+
9
+ attribute :blocks do
10
+ object.enabled_blocks.map do |block_name|
11
+ block(block_name)
12
+ end
13
+ end
14
+
15
+ attribute :metadata_blocks do
16
+ object.metadata_blocks.map do |block_name|
17
+ block(block_name)
18
+ end
19
+ end
20
+
21
+ attribute :images do
22
+ object.value(:images) || object.value(:image)
23
+ end
24
+
25
+ %i[dates tags files].each do |attr|
26
+ attribute attr do
27
+ object.value(attr)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def block(block_name)
34
+ reify_options(object.block(block_name).merge(name: block_name))
35
+ end
36
+
37
+ def reify_options(block)
38
+ return block unless block.key?(:options)
39
+
40
+ opts = block[:options]
41
+ opts = opts.call if opts.is_a?(Proc)
42
+ unless opts.present? && opts.first.is_a?(Array)
43
+ opts = opts.map { |v| [v, v] }
44
+ end
45
+ opts = ([["", nil]] + opts).uniq
46
+
47
+ block.merge(options: opts)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,22 @@
1
+ <% content_for :page_title, "Account recovery" %>
2
+ <% content_for :page_description, "Account recovery" %>
3
+
4
+ <%= form_tag admin_account_recovery_path do %>
5
+ <h2>
6
+ Forgot your password or lost your authenticator?
7
+ </h2>
8
+ <p>
9
+ Don't worry, it happens.
10
+ Enter your email address below, and we'll send you a link where you
11
+ can recover your account.
12
+ </p>
13
+ <div class="field">
14
+ <label for="email">Email address</label>
15
+ <%= text_field_tag(:email, "", autocomplete: "email", autofocus: true) %>
16
+ </div>
17
+ <p>
18
+ <button type="submit">
19
+ Send
20
+ </button>
21
+ </p>
22
+ <% end %>
@@ -0,0 +1,37 @@
1
+ <% content_for :page_title, "Account recovery" %>
2
+ <% content_for :page_description, "Please choose a new password to proceed" %>
3
+ <% content_for :body_class, "login" %>
4
+
5
+ <div class="login-form">
6
+ <%= form_for(@user,
7
+ url: admin_account_recovery_path,
8
+ builder: PagesCore::Admin::FormBuilder,
9
+ class: 'form') do |f| %>
10
+ <%= hidden_field_tag :token, @token %>
11
+ <%= f.labelled_password_field(:password,
12
+ autofocus: true,
13
+ autocomplete: "new-password") %>
14
+ <%= f.labelled_password_field(:password_confirmation,
15
+ autocomplete: "new-password") %>
16
+
17
+ <% if @user.otp_enabled? %>
18
+ <div class="field">
19
+ <label for="otp">6 digit code or recovery code</label>
20
+ <%= text_field_tag(:otp, "",
21
+ autocomplete: "one-time-code",
22
+ size: 6) %>
23
+ </div>
24
+ <p>
25
+ Lost your authenticator device? You can use one of your
26
+ emergency recovery codes instead.
27
+ </p>
28
+ <% end %>
29
+
30
+ <p>
31
+ <button type="submit">
32
+ Change password
33
+ </button>
34
+ or <%= link_to "Return to login screen", admin_login_path %>
35
+ </p>
36
+ <% end %>
37
+ </div>
@@ -14,7 +14,7 @@
14
14
  <%= f.labelled_text_field :email, autocomplete: "email" %>
15
15
  <%= f.labelled_password_field(:password,
16
16
  autocomplete: "new-password") %>
17
- <%= f.labelled_password_field(:confirm_password,
17
+ <%= f.labelled_password_field(:password_confirmation,
18
18
  autocomplete: "new-password") %>
19
19
  <p>
20
20
  <button type="submit">
@@ -19,9 +19,8 @@
19
19
  <h2>
20
20
  <%= link_to_unless_current(
21
21
  year,
22
- admin_news_index_path(locale, year: year, category: category&.slug)
22
+ admin_news_index_path(locale, year: year)
23
23
  ) %>
24
- <%= ": #{@category.name}" if @category %>
25
24
  <span class="count">
26
25
  (<%= year_count %>)
27
26
  </span>
@@ -37,8 +36,7 @@
37
36
  <% else %>
38
37
  <%= link_to(
39
38
  month_name(month),
40
- admin_news_index_path(locale, year: year, month: month,
41
- category: category&.slug)
39
+ admin_news_index_path(locale, year: year, month: month)
42
40
  ) %>
43
41
  <span class="count">
44
42
  (<%= page_count %>)
@@ -18,7 +18,6 @@
18
18
  <%= render(partial: "sidebar",
19
19
  locals: {
20
20
  locale: content_locale,
21
- category: @category,
22
21
  news_pages: @news_pages,
23
22
  archive_finder: @archive_finder
24
23
  }) %>
@@ -0,0 +1,7 @@
1
+ <% content_for :page_title, "2FA enabled" %>
2
+ <% content_for :page_description, "Two-factor authentication enabled" %>
3
+
4
+ <div class="content">
5
+ <%= render(partial: "admin/recovery_codes/codes",
6
+ locals: { recovery_codes: @recovery_codes }) %>
7
+ </div>
@@ -0,0 +1,60 @@
1
+ <% content_for :page_title, "Enable 2FA" %>
2
+ <% content_for :page_description, "Enable two-factor authentication" %>
3
+
4
+ <%= form_tag(admin_otp_secret_path, method: :post, class: "totp-enrollment") do |f| %>
5
+ <h2>
6
+ Scan the QR-code
7
+ </h2>
8
+ <p>
9
+ Use an authenticator app or browser extension to scan the QR code below.<br>
10
+ Don't have one? Some options are
11
+ <%= link_to("1Password", "https://1password.com/") %>,
12
+ <%= link_to("LastPass Authenticator", "https://www.lastpass.com/") %>,
13
+ <%= link_to("Microsoft Authenticator",
14
+ "https://www.microsoft.com/en-us/security/mobile-authenticator-app") %>
15
+ or
16
+ <%= link_to("Google Authenticator",
17
+ "https://support.google.com/accounts/answer/1066447") %>.
18
+ </p>
19
+
20
+
21
+ <div class="qr-code">
22
+ <%= qr_code(@otp_secret.provisioning_uri) %>
23
+ </div>
24
+
25
+ <p>
26
+ If you are unable to scan the code, you can enter the following
27
+ info instead:
28
+ </p>
29
+
30
+ <p>
31
+ <b>Account name:</b><br>
32
+ <%= @otp_secret.account_name %>
33
+ </p>
34
+ <p>
35
+ <b>Secret:</b><br>
36
+ <span class="otp-secret">
37
+ <%= @otp_secret.secret %>
38
+ </span>
39
+ </p>
40
+
41
+ <h2>
42
+ Enter the code from the app
43
+ </h2>
44
+
45
+ <div class="field">
46
+ <label for="otp">6 digit code</label>
47
+ <%= text_field_tag(:otp, "",
48
+ autofocus: true,
49
+ autocomplete: "one-time-code",
50
+ size: 6) %>
51
+ </div>
52
+
53
+ <%= hidden_field_tag :signed_message, @otp_secret.signed_message %>
54
+
55
+ <p>
56
+ <button type="submit">
57
+ Verify
58
+ </button>
59
+ </p>
60
+ <% end %>