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
@@ -1,31 +1,11 @@
1
- <%= content_tab "Content" do %>
2
- <%= render partial: "edit_content", locals: { f: f } %>
3
- <% end %>
4
-
5
- <% if @page.unconfigured_blocks.any? %>
6
- <%= content_tab "Unconfigured content" do %>
7
- <p>
8
- This page has additional content fields not enabled by the
9
- selected template.
10
- </p>
11
- <% @page.unconfigured_blocks do |block_name, block_options| %>
12
- <%= page_block_field(f, block_name, block_options) %>
13
- <% end %>
14
- <% end %>
15
- <% end %>
16
-
17
- <% if @page.template_config.value(:images) || @page.template_config.value(:image) %>
18
- <%= content_tab "Images" do %>
19
- <%= render partial: "edit_images", locals: { f: f } %>
20
- <% end %>
21
- <% end %>
22
-
23
- <% if @page.template_config.value(:files) %>
24
- <%= content_tab "Files" do %>
25
- <%= render partial: "edit_files", locals: { f: f } %>
26
- <% end %>
27
- <% end %>
28
-
29
- <%= content_tab "Metadata" do %>
30
- <%= render partial: "edit_metadata", locals: { f: f } %>
1
+ <% content_for :main_wrapper do %>
2
+ <%= react_component(
3
+ "PageForm",
4
+ { locale: content_locale,
5
+ locales: locales_with_dir,
6
+ page: Admin::PageResource.new(page, params: { user: current_user }),
7
+ templates: PagesCore::Templates.all.map { |t| Admin::TemplateConfigurationResource.new(t) },
8
+ authors: page_authors(page).map{ |a| [a.name, a.id] },
9
+ statuses: Page.status_labels }
10
+ ) %>
31
11
  <% end %>
@@ -1,7 +1,7 @@
1
1
  <% query ||= "" %>
2
2
  <%= form_tag(search_admin_pages_path(content_locale),
3
3
  method: "get",
4
- class: "search-bar") do %>
4
+ class: "search-bar inline-form") do %>
5
5
  <%= text_field_tag(:q, query,
6
6
  placeholder: "Search all pages",
7
7
  aria: { label: "Search all pages" }) %>
@@ -5,60 +5,4 @@
5
5
  Edit page
6
6
  <% end %>
7
7
  <% end %>
8
- <% content_for :page_description do %>
9
- Editing
10
- <% @page.ancestors.reverse.each do |page| %>
11
- <%= link_to(page.name? ? page.name : tag.i("Untitled"),
12
- edit_admin_page_path(content_locale, page)) %>
13
- &raquo;
14
- <% end %>
15
- <%= link_to(@page.name? ? @page.name : tag.i("Untitled"),
16
- edit_admin_page_path(content_locale, @page)) %>
17
- <% end %>
18
-
19
- <% content_for :page_description_links do %>
20
- <%= locale_links { |l| edit_admin_page_path(l, @page.localize(l)) } %>
21
- <% end %>
22
-
23
- <% content_for :main_wrapper do %>
24
- <%= form_for(@page,
25
- url: admin_page_url(content_locale, @page),
26
- builder: PagesCore::Admin::FormBuilder,
27
- html: {
28
- class: "edit-page main-wrapper",
29
- method: :put,
30
- data: {
31
- controller: "edit-page",
32
- "edit-page-target": "form",
33
- "preview-url": preview_page_url(@page.locale, @page)
34
- }
35
- }) do |f| %>
36
-
37
- <% content_for :main do %>
38
- <div class="content">
39
- <%= render(partial: "form", locals: { f: f }) %>
40
-
41
- <div class="buttons">
42
- <button type="button"
43
- id="previewButton"
44
- data-action="click->edit-page#preview"
45
- data-url="<%= preview_page_url(@page.locale, @page) %>">
46
- Preview
47
- </button>
48
- <button type="submit">
49
- Save
50
- </button>
51
- </div>
52
- </div>
53
- <% end %>
54
-
55
- <main data-controller="main">
56
- <%= render(partial: "layouts/admin/page_header") %>
57
- <%= yield :main %>
58
- </main>
59
-
60
- <aside class="sidebar" id="page-form-sidebar">
61
- <%= render partial: 'edit_options', locals: { f: f } %>
62
- </aside>
63
- <% end %>
64
- <% end %>
8
+ <%= render(partial: "form", locals: { page: @page }) %>
@@ -16,7 +16,7 @@
16
16
  <% cache Page.visible.roots.to_a + [current_user, content_locale] do %>
17
17
  <%= react_component(
18
18
  "PageTree", {
19
- pages: @pages.map { |p| page_json(p) },
19
+ pages: @pages.map { |p| ::Admin::PageTreeResource.new(p, params: { user: current_user }) },
20
20
  locale: content_locale,
21
21
  dir: locale_direction(content_locale),
22
22
  permissions: [(:create if policy(Page).create?)] }
@@ -1,45 +1,2 @@
1
1
  <% content_for :page_title, "New page" %>
2
- <% content_for :page_description do %>
3
- <% if @page.parent %>
4
- <em><%= @page.parent.name %></em> &raquo; New Page
5
- <% else %>
6
- You are creating a new root page
7
- <% end %>
8
- <% end %>
9
-
10
- <% content_for :main_wrapper do %>
11
- <%= form_for(@page,
12
- url: admin_pages_url(content_locale),
13
- builder: PagesCore::Admin::FormBuilder,
14
- html: {
15
- class: "edit-page main-wrapper",
16
- data: {
17
- controller: "edit-page",
18
- "edit-page-target": "form"
19
- }
20
- }) do |f| %>
21
-
22
- <% content_for :main do %>
23
- <div class="content">
24
- <%= f.hidden_field "parent_page_id" if @page.parent %>
25
-
26
- <%= render(partial: "form", locals: { f: f }) %>
27
-
28
- <div class="buttons">
29
- <button type="submit">
30
- Save
31
- </button>
32
- </div>
33
- </div>
34
- <% end %>
35
-
36
- <main data-controller="main">
37
- <%= render(partial: "layouts/admin/page_header") %>
38
- <%= yield :main %>
39
- </main>
40
-
41
- <aside class="sidebar" id="page-form-sidebar">
42
- <%= render partial: "edit_options", locals: { f: f } %>
43
- </aside>
44
- <% end %>
45
- <% end %>
2
+ <%= render(partial: "form", locals: { page: @page }) %>
@@ -0,0 +1,14 @@
1
+ <h2>
2
+ Recovery codes
3
+ </h2>
4
+ <p>
5
+ Please save the recovery codes below in a safe place, ideally
6
+ using a secure password manager.<br>
7
+ Without them, you will lose access to your account if you lose your device.
8
+ </p>
9
+
10
+ <ul class="recovery-codes">
11
+ <% recovery_codes.each do |c| %>
12
+ <li><%= c %></li>
13
+ <% end %>
14
+ </ul>
@@ -0,0 +1,7 @@
1
+ <% content_for :page_title, "New recovery codes" %>
2
+ <% content_for :page_description, "Recovery codes updated" %>
3
+
4
+ <div class="content">
5
+ <%= render(partial: "admin/recovery_codes/codes",
6
+ locals: { recovery_codes: @recovery_codes }) %>
7
+ </div>
@@ -0,0 +1,11 @@
1
+ <% content_for :page_title, "New recovery codes" %>
2
+ <% content_for :page_description, "Generate new recovery codes" %>
3
+
4
+ <%= form_tag(admin_recovery_codes_path, method: :post) do |f| %>
5
+ <%= render(partial: "admin/sessions/otp_form") %>
6
+ <p>
7
+ <button type="submit">
8
+ Verify
9
+ </button>
10
+ </p>
11
+ <% end %>
@@ -0,0 +1,13 @@
1
+ <h2>
2
+ Two-factor authentication
3
+ </h2>
4
+ <p>
5
+ Enter a one-time code from your authenticator app to proceed.
6
+ </p>
7
+ <div class="field">
8
+ <label for="otp">6 digit code</label>
9
+ <%= text_field_tag(:otp, "",
10
+ autofocus: true,
11
+ autocomplete: "one-time-code",
12
+ size: 6) %>
13
+ </div>
@@ -0,0 +1,31 @@
1
+ <% content_for :page_title, "Sign in" %>
2
+ <% content_for(:page_description,
3
+ "Please enter your email address and password to sign in") %>
4
+ <% content_for :body_class, "login" %>
5
+
6
+ <% content_for :sidebar do %>
7
+ <h2>Please note</h2>
8
+ <p>
9
+ Please contact support if you experience problems logging in or using Pages.
10
+ </p>
11
+ <% end %>
12
+
13
+ <div class="login-form">
14
+ <%= form_tag admin_session_path do %>
15
+ <div class="field">
16
+ <label>Email address</label>
17
+ <%= text_field_tag(:email, "", autocomplete: "email") %>
18
+ </div>
19
+ <div class="field">
20
+ <label>Password</label>
21
+ <%= password_field_tag(:password, "", autocomplete: "current-password") %>
22
+ </div>
23
+ <div class="buttons">
24
+ <button type="submit">Sign in</button>
25
+ </div>
26
+ <p>
27
+ <%= link_to("<b>Help!</b> I forgot my password!".html_safe,
28
+ new_admin_account_recovery_path) %>
29
+ </p>
30
+ <% end %>
31
+ </div>
@@ -0,0 +1,19 @@
1
+ <% content_for :page_title, "Two-factor authentication" %>
2
+ <% content_for :page_description, "Two-factor authentication" %>
3
+
4
+ <%= form_tag(verify_otp_admin_session_path, method: :post) do |f| %>
5
+ <%= hidden_field_tag :signed_user_id, @signed_user_id %>
6
+ <%= render(partial: "admin/sessions/otp_form") %>
7
+
8
+ <p>
9
+ <button type="submit">
10
+ Verify
11
+ </button>
12
+ </p>
13
+
14
+ <p>
15
+ Lost your authenticator device?
16
+ <%= link_to("Recover your account here",
17
+ new_admin_account_recovery_path) %>.
18
+ </p>
19
+ <% end %>
@@ -4,7 +4,11 @@
4
4
  </h2>
5
5
  <p>
6
6
  <% if f.object.kind_of?(User) && f.object != current_user %>
7
- <%= f.check_box :activated %> The user account is activated<br />
7
+ <%= f.check_box :activated %>
8
+ <label for="user_activated">
9
+ The user account is activated
10
+ </label>
11
+ <br />
8
12
  <% end %>
9
13
  <% Role.roles.each do |role| %>
10
14
  <%= check_box_tag("#{model_name_from_record_or_class(f.object).param_key}[role_names][]",
@@ -3,8 +3,9 @@
3
3
  <th>Name</th>
4
4
  <th>Email</th>
5
5
  <th>Can access</th>
6
+ <th>2FA</th>
6
7
  <th>Last seen</th>
7
- <th></th>
8
+ <th colspan="2"></th>
8
9
  </tr>
9
10
  <% @invites.each do |invite| %>
10
11
  <tr class="invite">
@@ -15,13 +16,14 @@
15
16
  <td>
16
17
  <%= invite.roles.map(&:to_s).sort.to_sentence %>
17
18
  </td>
19
+ <td></td>
18
20
  <td>
19
21
  <% if invite.sent_at? %>
20
22
  Invited
21
23
  <%= time_ago_in_words(invite.sent_at) %> ago
22
24
  <% end %>
23
25
  </td>
24
- <td>
26
+ <td colspan="2">
25
27
  <% if current_user.role?(:users) %>
26
28
  <%= link_to("View invite",
27
29
  admin_invite_with_token_url(invite, invite.token)) %> /
@@ -36,16 +38,14 @@
36
38
  <% end %>
37
39
  <% @users.each do |user| -%>
38
40
  <tr class="user-<%= user.id %>">
39
- <td>
40
- <strong><%= link_to user.name, admin_user_url( user ) %></strong>
41
- <% if policy(user).edit? %>
42
- (<%= link_to "edit", edit_admin_user_url( user ), class: :edit %>)
43
- <% end %>
41
+ <td class="name">
42
+ <%= link_to user.name, admin_user_url( user ) %>
44
43
  </td>
45
44
  <td>
46
45
  <%= user.email %>
47
46
  </td>
48
47
  <td><%= user.roles.map(&:to_s).sort.to_sentence %></td>
48
+ <td><%= user.otp_enabled? ? "Enabled" : "" %></td>
49
49
  <td>
50
50
  <% if user.online? -%>
51
51
  <strong>Online now</strong>
@@ -62,6 +62,11 @@
62
62
  notes.join( ", " )
63
63
  %>
64
64
  </td>
65
+ <td>
66
+ <% if policy(user).edit? %>
67
+ <%= link_to("Edit", edit_admin_user_url(user), class: :edit) %>
68
+ <% end %>
69
+ </td>
65
70
  </tr>
66
71
  <% end -%>
67
72
  </table>
@@ -31,12 +31,42 @@
31
31
  <% if policy(@user).change_password? %>
32
32
  <h2>Password</h2>
33
33
  <%= f.labelled_password_field :password, 'Change password' %>
34
- <%= f.labelled_password_field :confirm_password, 'Confirm password' %>
34
+ <%= f.labelled_password_field :password_confirmation, 'Confirm password' %>
35
35
  <p>
36
36
  Leave the password blank if you do not wish to change the password.
37
37
  </p>
38
38
  <% end %>
39
39
 
40
+ <% if policy(@user).otp? %>
41
+ <h2>Two-factor authentication</h2>
42
+ <% if @user.otp_enabled? %>
43
+ <p>
44
+ Two-factor authentication has been enabled.
45
+ <%= link_to("Disable",
46
+ admin_otp_secret_path,
47
+ class: :delete,
48
+ method: :delete,
49
+ data: { confirm: "Are you sure you want to disable 2FA?" }) %>
50
+ </p>
51
+ <p>
52
+
53
+ You have
54
+ <%= t("pages_core.recovery_codes",
55
+ count: @user.hashed_recovery_codes.length) %>
56
+ remaining.
57
+ <%= link_to("Generate new codes", new_admin_recovery_codes_path) %>
58
+ </p>
59
+ <% else %>
60
+ <p>
61
+ Protect your account with an additional layer of security by
62
+ requiring an authentication app to sign in.
63
+ </p>
64
+ <p>
65
+ <%= link_to("Enable 2FA", new_admin_otp_secret_path) %>
66
+ </p>
67
+ <% end %>
68
+ <% end %>
69
+
40
70
  <%= render partial: "access_control", locals: { user: @user, f: f } %>
41
71
 
42
72
  <p>
@@ -11,7 +11,7 @@
11
11
  <%= f.labelled_text_field(:email, autocomplete: "email") %>
12
12
  <%= f.labelled_password_field(:password,
13
13
  autocomplete: "new-password") %>
14
- <%= f.labelled_password_field(:confirm_password,
14
+ <%= f.labelled_password_field(:password_confirmation,
15
15
  autocomplete: "new-password") %>
16
16
 
17
17
  <p>
@@ -0,0 +1,10 @@
1
+ Hi, <%= @user.name %>!
2
+
3
+ We've received a request to recover your account on <%= PagesCore.config(:site_name) %>.
4
+
5
+ Please click the following link to continue:
6
+ <%= @url %>
7
+
8
+ The link will expire in 24 hours.
9
+
10
+ If you do not want to recover your password, please ignore this email.
@@ -1,16 +1,14 @@
1
1
  <header>
2
- <div class="logo">
3
- <%= link_to image_tag("pages/admin/icon.svg"), '/admin' %>
4
- </div>
5
2
  <div class="site-name">
6
3
  <h1>
7
- <%= link_to "Pages", "/admin" %> <%= PagesCore.config :site_name %>
4
+ <%= link_to("Pages", "/admin", class: "logo") %>
5
+ <%= PagesCore.config :site_name %>
8
6
  </h1>
9
7
  </div>
10
8
  <% if logged_in? %>
11
9
  <div class="user">
12
10
  Hello, <%= link_to(current_user.name, admin_user_url(current_user)) %>
13
- <%= link_to("Log out", session_path, method: "delete") %>
11
+ <%= link_to("Log out", admin_session_path, method: "delete") %>
14
12
  </div>
15
13
  <% end %>
16
14
  <nav class="tabs">
@@ -13,8 +13,7 @@
13
13
  role="tablist">
14
14
  <% content_tabs.map do |t| %>
15
15
  <li id="content-tab-link-<%= t[:key] %>"
16
- data-tab="<%= t[:key] %>"
17
- data-main-target="link">
16
+ data-tab="<%= t[:key] %>">
18
17
  <% if t[:options][:disabled] == true %>
19
18
  <%= t[:name] %>
20
19
  <% else %>
@@ -0,0 +1,12 @@
1
+ <%= react_component "Toast", { notice: flash[:notice], error: flash[:error] } %>
2
+ <% if Rails.env.test? && flash.any? %>
3
+ <div class="flash-test-helper">
4
+ <% %i[notice error].each do |type| %>
5
+ <% if flash[type] || true %>
6
+ <div class="<%= type %>">
7
+ <%= flash[type] %>
8
+ </div>
9
+ <% end %>
10
+ <% end %>
11
+ </div>
12
+ <% end %>
@@ -44,7 +44,7 @@
44
44
  <%= yield :main_wrapper %>
45
45
  <% else %>
46
46
  <div class="main-wrapper">
47
- <main data-controller="main">
47
+ <main>
48
48
  <%= render(partial: "layouts/admin/page_header") %>
49
49
  <%= yield %>
50
50
  </main>
@@ -58,6 +58,6 @@
58
58
  <% end %>
59
59
  </div>
60
60
  <%= react_component "Modal", {} %>
61
- <%= react_component "Toast", { notice: flash[:notice], error: flash[:error] } %>
61
+ <%= render(partial: "layouts/admin/toast") %>
62
62
  </body>
63
63
  </html>
@@ -19,22 +19,26 @@ en:
19
19
  image: Profile picture
20
20
  pages_core:
21
21
  account_holder_exists: Account holder already exists
22
- categories_controller:
23
- created: New category created
24
- deleted: Category was deleted
25
- updated: Category was updated
26
22
  changes_saved: Your changes were saved
27
23
  invalid_login: >
28
24
  The provided email address and password combination was not valid
29
25
  invite_expired: This invite is no longer valid.
30
26
  logged_out: You have been logged out
31
- password_reset:
27
+ otp:
28
+ already_enabled: 2FA has already been enabled
29
+ disabled: 2FA has been disabled
30
+ invalid_code: Invalid 2FA code
31
+ required: 2FA is required for this
32
+ account_recovery:
32
33
  changed: Your password has been changed
33
- expired: Your password reset link has expired
34
- invalid_request: Invalid password reset request
34
+ invalid_request: This link is no longer valid
35
35
  not_found: Couldn't find a user with that email address
36
36
  sent: An email with further instructions has been sent
37
37
  problems_saving: There were problems saving your changes
38
+ recovery_codes:
39
+ zero: "no recovery codes"
40
+ one: "one recovery code"
41
+ other: "%{count} recovery codes"
38
42
  templates:
39
43
  default:
40
44
  blocks:
data/config/routes.rb CHANGED
@@ -17,9 +17,7 @@ Rails.application.routes.draw do
17
17
  collection do
18
18
  get "search"
19
19
  post "search"
20
- end
21
- member do
22
- put "preview"
20
+ post "preview"
23
21
  end
24
22
  resources :files, controller: "page_files"
25
23
  end
@@ -33,13 +31,12 @@ Rails.application.routes.draw do
33
31
  get "pages/:locale/*glob" => redirect("/%{locale}/pages/%{glob}"),
34
32
  locale: /\w\w\w/
35
33
 
36
- # Authentication
37
- resource :session, only: %i[create destroy]
38
-
39
34
  # Sitemap
40
35
  resource :sitemap, only: [:show]
41
36
 
42
37
  namespace :admin do
38
+ get "users/login" => redirect("/admin/login")
39
+
43
40
  # Invites
44
41
  resources :invites do
45
42
  member do
@@ -51,9 +48,9 @@ Rails.application.routes.draw do
51
48
  end
52
49
 
53
50
  # Password resets
54
- resources :password_resets, only: %i[create show update]
55
- controller :password_resets do
56
- get "/password_resets/:id/:token" => :show, as: :password_reset_with_token
51
+ resource :account_recovery
52
+ controller :account_recoveries do
53
+ get "/account_recovery/:token" => :show, as: :account_recovery_with_token
57
54
  end
58
55
 
59
56
  # Attachments
@@ -66,15 +63,19 @@ Rails.application.routes.draw do
66
63
  resources :users do
67
64
  collection do
68
65
  get "deactivated"
69
- get "login"
70
66
  end
71
67
  member do
72
68
  delete "delete_image"
73
69
  end
74
70
  end
75
71
 
76
- # Categories
77
- resources :categories
72
+ # Authentication
73
+ resource :session, only: %i[create destroy] do
74
+ member { post :verify_otp }
75
+ end
76
+ resource :otp_secret, only: %i[new create destroy]
77
+ resource :recovery_codes, only: %i[new create]
78
+ get "login" => "sessions#new", as: "login"
78
79
 
79
80
  # Pages
80
81
  scope ":locale" do
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Add2faFields < ActiveRecord::Migration[7.0]
4
+ class User < ApplicationRecord; end
5
+
6
+ def change
7
+ change_table :users do |t|
8
+ t.boolean :otp_enabled, null: false, default: false
9
+ t.string :otp_secret
10
+ t.datetime :last_otp_at
11
+ t.jsonb :hashed_recovery_codes, null: false, default: []
12
+ t.string :session_token
13
+ end
14
+
15
+ rename_column :users, :hashed_password, :password_digest
16
+
17
+ reversible do |dir|
18
+ dir.up do
19
+ User.find_each do |u|
20
+ u.update_columns(session_token: SecureRandom.hex(32))
21
+ end
22
+ change_column_null :users, :session_token, false
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RemovePasswordResetTokens < ActiveRecord::Migration[7.0]
4
+ def change
5
+ drop_table :password_reset_tokens do |t|
6
+ t.integer :user_id
7
+ t.string :token
8
+ t.datetime :expires_at
9
+ t.datetime :created_at
10
+ t.datetime :updated_at
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ChangeEmailToCitext < ActiveRecord::Migration[7.0]
4
+ def up
5
+ enable_extension "citext"
6
+ %i[users invites].each do |t|
7
+ change_column t, :email, :citext
8
+ add_index t, :email, unique: true, name: "index_#{t}_on_email"
9
+ end
10
+ end
11
+
12
+ def down
13
+ %i[users invites].each do |t|
14
+ change_column t, :email, :string
15
+ remove_index t, name: "index_#{t}_on_email"
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RemovePersistentData < ActiveRecord::Migration[7.0]
4
+ def change
5
+ remove_column :users, :persistent_data, :text
6
+ end
7
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RemoveCategories < ActiveRecord::Migration[7.0]
4
+ def change
5
+ drop_table :page_categories do |t|
6
+ t.integer :page_id
7
+ t.integer :category_id
8
+ t.index :category_id
9
+ t.index :page_id
10
+ end
11
+
12
+ drop_table :categories do |t|
13
+ t.string :name
14
+ t.string :slug
15
+ t.integer :position
16
+ t.datetime :created_at, null: false
17
+ t.datetime :updated_at, null: false
18
+ t.index :slug
19
+ end
20
+ end
21
+ end