cms42 0.1.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 (145) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README.md +299 -0
  4. data/Rakefile +10 -0
  5. data/app/assets/stylesheets/cms/application.css +3 -0
  6. data/app/controllers/cms/admin/api_keys_controller.rb +52 -0
  7. data/app/controllers/cms/admin/base_controller.rb +54 -0
  8. data/app/controllers/cms/admin/documents_controller.rb +56 -0
  9. data/app/controllers/cms/admin/form_fields_controller.rb +70 -0
  10. data/app/controllers/cms/admin/form_submissions_controller.rb +36 -0
  11. data/app/controllers/cms/admin/images_controller.rb +67 -0
  12. data/app/controllers/cms/admin/pages_controller.rb +188 -0
  13. data/app/controllers/cms/admin/sections_controller.rb +177 -0
  14. data/app/controllers/cms/admin/sites_controller.rb +60 -0
  15. data/app/controllers/cms/admin/webhook_deliveries_controller.rb +19 -0
  16. data/app/controllers/cms/admin/webhooks_controller.rb +51 -0
  17. data/app/controllers/cms/api/base_controller.rb +45 -0
  18. data/app/controllers/cms/api/v1/base_controller.rb +29 -0
  19. data/app/controllers/cms/api/v1/pages_controller.rb +21 -0
  20. data/app/controllers/cms/api/v1/sites_controller.rb +18 -0
  21. data/app/controllers/cms/application_controller.rb +11 -0
  22. data/app/controllers/cms/public/base_controller.rb +23 -0
  23. data/app/controllers/cms/public/form_submissions_controller.rb +63 -0
  24. data/app/controllers/cms/public/previews_controller.rb +21 -0
  25. data/app/controllers/cms/public/sites_controller.rb +37 -0
  26. data/app/controllers/concerns/cms/admin/page_scoped_sections.rb +42 -0
  27. data/app/controllers/concerns/cms/current_site_resolver.rb +17 -0
  28. data/app/controllers/concerns/cms/public/page_paths.rb +31 -0
  29. data/app/controllers/concerns/cms/public/page_rendering.rb +23 -0
  30. data/app/controllers/concerns/cms/site_resolvable.rb +27 -0
  31. data/app/helpers/cms/admin/pages_helper.rb +29 -0
  32. data/app/helpers/cms/admin/sections_helper.rb +86 -0
  33. data/app/helpers/cms/admin/sites_helper.rb +8 -0
  34. data/app/helpers/cms/application_helper.rb +51 -0
  35. data/app/helpers/cms/media_helper.rb +28 -0
  36. data/app/helpers/cms/pages_helper.rb +16 -0
  37. data/app/helpers/cms/sections_helper.rb +25 -0
  38. data/app/helpers/cms/sites_helper.rb +11 -0
  39. data/app/javascript/cms/controllers/sortable_controller.js +38 -0
  40. data/app/jobs/cms/application_job.rb +6 -0
  41. data/app/jobs/cms/deliver_webhook_job.rb +66 -0
  42. data/app/mailers/cms/application_mailer.rb +9 -0
  43. data/app/mailers/cms/form_submission_mailer.rb +16 -0
  44. data/app/models/cms/api_key.rb +27 -0
  45. data/app/models/cms/application_record.rb +7 -0
  46. data/app/models/cms/document.rb +48 -0
  47. data/app/models/cms/form_field.rb +27 -0
  48. data/app/models/cms/form_submission.rb +42 -0
  49. data/app/models/cms/image.rb +61 -0
  50. data/app/models/cms/image_translation.rb +22 -0
  51. data/app/models/cms/page.rb +228 -0
  52. data/app/models/cms/page_section.rb +43 -0
  53. data/app/models/cms/page_translation.rb +22 -0
  54. data/app/models/cms/section/block_base.rb +32 -0
  55. data/app/models/cms/section/blocks/call_to_action_block.rb +16 -0
  56. data/app/models/cms/section/blocks/hero_block.rb +14 -0
  57. data/app/models/cms/section/blocks/image_block.rb +13 -0
  58. data/app/models/cms/section/blocks/rich_text_block.rb +12 -0
  59. data/app/models/cms/section/kind_registry.rb +66 -0
  60. data/app/models/cms/section.rb +94 -0
  61. data/app/models/cms/section_image.rb +10 -0
  62. data/app/models/cms/section_translation.rb +25 -0
  63. data/app/models/cms/site.rb +87 -0
  64. data/app/models/cms/webhook.rb +41 -0
  65. data/app/models/cms/webhook_delivery.rb +12 -0
  66. data/app/serializers/cms/api/base_serializer.rb +51 -0
  67. data/app/serializers/cms/api/page_serializer.rb +145 -0
  68. data/app/serializers/cms/api/site_serializer.rb +45 -0
  69. data/app/services/cms/locale_resolver.rb +30 -0
  70. data/app/services/cms/page_resolver.rb +73 -0
  71. data/app/services/cms/public_page_context.rb +49 -0
  72. data/app/views/cms/admin/api_keys/_form.html.erb +23 -0
  73. data/app/views/cms/admin/api_keys/create.html.erb +9 -0
  74. data/app/views/cms/admin/api_keys/edit.html.erb +5 -0
  75. data/app/views/cms/admin/api_keys/index.html.erb +36 -0
  76. data/app/views/cms/admin/api_keys/new.html.erb +5 -0
  77. data/app/views/cms/admin/documents/_form.html.erb +24 -0
  78. data/app/views/cms/admin/documents/edit.html.erb +2 -0
  79. data/app/views/cms/admin/documents/index.html.erb +37 -0
  80. data/app/views/cms/admin/documents/new.html.erb +2 -0
  81. data/app/views/cms/admin/form_fields/_form.html.erb +46 -0
  82. data/app/views/cms/admin/form_fields/edit.html.erb +2 -0
  83. data/app/views/cms/admin/form_fields/index.html.erb +41 -0
  84. data/app/views/cms/admin/form_fields/new.html.erb +2 -0
  85. data/app/views/cms/admin/form_submissions/index.html.erb +38 -0
  86. data/app/views/cms/admin/images/_form.html.erb +36 -0
  87. data/app/views/cms/admin/images/edit.html.erb +2 -0
  88. data/app/views/cms/admin/images/index.html.erb +25 -0
  89. data/app/views/cms/admin/images/new.html.erb +2 -0
  90. data/app/views/cms/admin/pages/_attach_section_panel.html.erb +20 -0
  91. data/app/views/cms/admin/pages/_form.html.erb +116 -0
  92. data/app/views/cms/admin/pages/_section_editor_frame.html.erb +3 -0
  93. data/app/views/cms/admin/pages/_sections_list.html.erb +9 -0
  94. data/app/views/cms/admin/pages/edit.html.erb +2 -0
  95. data/app/views/cms/admin/pages/index.html.erb +62 -0
  96. data/app/views/cms/admin/pages/new.html.erb +2 -0
  97. data/app/views/cms/admin/pages/show.html.erb +111 -0
  98. data/app/views/cms/admin/sections/_form.html.erb +128 -0
  99. data/app/views/cms/admin/sections/_section.html.erb +22 -0
  100. data/app/views/cms/admin/sections/edit.html.erb +9 -0
  101. data/app/views/cms/admin/sections/index.html.erb +47 -0
  102. data/app/views/cms/admin/sections/new.html.erb +9 -0
  103. data/app/views/cms/admin/sections/page_update.turbo_stream.erb +17 -0
  104. data/app/views/cms/admin/sections/show.html.erb +97 -0
  105. data/app/views/cms/admin/sites/_form.html.erb +44 -0
  106. data/app/views/cms/admin/sites/edit.html.erb +3 -0
  107. data/app/views/cms/admin/sites/new.html.erb +5 -0
  108. data/app/views/cms/admin/sites/show.html.erb +22 -0
  109. data/app/views/cms/admin/webhook_deliveries/index.html.erb +29 -0
  110. data/app/views/cms/admin/webhooks/_form.html.erb +38 -0
  111. data/app/views/cms/admin/webhooks/edit.html.erb +5 -0
  112. data/app/views/cms/admin/webhooks/index.html.erb +34 -0
  113. data/app/views/cms/admin/webhooks/new.html.erb +5 -0
  114. data/app/views/cms/form_submission_mailer/notify.html.erb +14 -0
  115. data/app/views/cms/form_submission_mailer/notify.text.erb +7 -0
  116. data/app/views/cms/public/pages/_content.html.erb +48 -0
  117. data/app/views/cms/public/pages/show.html.erb +44 -0
  118. data/app/views/cms/public/pages/templates/_custom.html.erb +3 -0
  119. data/app/views/cms/public/pages/templates/_form.html.erb +3 -0
  120. data/app/views/cms/public/pages/templates/_landing.html.erb +3 -0
  121. data/app/views/cms/public/pages/templates/_standard.html.erb +3 -0
  122. data/app/views/cms/sections/kinds/_cta.html.erb +13 -0
  123. data/app/views/cms/sections/kinds/_hero.html.erb +14 -0
  124. data/app/views/cms/sections/kinds/_image.html.erb +19 -0
  125. data/app/views/cms/sections/kinds/_rich_text.html.erb +6 -0
  126. data/app/views/layouts/cms/application.html.erb +13 -0
  127. data/app/views/layouts/cms/public.html.erb +14 -0
  128. data/bin/rails +19 -0
  129. data/bin/rubocop +9 -0
  130. data/cms.gemspec +29 -0
  131. data/config/importmap.rb +4 -0
  132. data/config/locales/activerecord.cms.en.yml +65 -0
  133. data/config/locales/en.yml +390 -0
  134. data/config/routes.rb +56 -0
  135. data/lib/cms/engine.rb +45 -0
  136. data/lib/cms/version.rb +5 -0
  137. data/lib/cms.rb +75 -0
  138. data/lib/cms42.rb +3 -0
  139. data/lib/generators/cms/install/install_generator.rb +26 -0
  140. data/lib/generators/cms/install/templates/create_cms_tables.rb +194 -0
  141. data/lib/generators/cms/install/templates/initializer.rb +21 -0
  142. data/lib/generators/cms/views/views_generator.rb +79 -0
  143. data/lib/tasks/cms_tasks.rake +6 -0
  144. data/lib/tasks/version.rake +8 -0
  145. metadata +281 -0
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cms
4
+ module Admin
5
+ class PagesController < BaseController
6
+ include Cms::Public::PageRendering
7
+ include Cms::Public::PagePaths
8
+
9
+ helper Cms::ApplicationHelper
10
+ helper Cms::MediaHelper
11
+ helper Cms::SitesHelper
12
+ helper Cms::SectionsHelper
13
+ helper Cms::PagesHelper
14
+
15
+ before_action :set_page, only: %i[show edit update destroy preview]
16
+
17
+ def index
18
+ pages = current_site.pages.kept.includes(:localised).ordered.to_a
19
+ depths = page_depths(pages)
20
+
21
+ @page_rows = if params[:q].present?
22
+ matching_page_rows(pages, depths)
23
+ else
24
+ flattened_page_rows(pages)
25
+ end
26
+ end
27
+
28
+ def show
29
+ @translation_locale = requested_locale
30
+ @subpages = @page.subpages.sort_by { |page| [page.position || 0, page.id] }
31
+ @page_sections = @page.page_sections.ordered.includes(:section)
32
+ @available_sections = current_site.sections
33
+ .kept
34
+ .global
35
+ .where.not(id: @page.section_ids)
36
+ .ordered
37
+ end
38
+
39
+ def preview
40
+ @translation_locale = requested_locale
41
+ @page.page_translations.find_or_initialize_by(locale: @translation_locale)
42
+ I18n.with_locale(@translation_locale) do
43
+ assign_public_page(site: current_site, page: @page)
44
+ render template: "cms/public/pages/show", layout: Cms.config.public_layout
45
+ end
46
+ end
47
+
48
+ def new
49
+ @page = current_site.pages.build
50
+ @translation_locale = requested_locale
51
+ @page.page_translations.build(locale: @translation_locale)
52
+ @parent_options = parent_options_for(nil)
53
+ end
54
+
55
+ def edit
56
+ @translation_locale = requested_locale
57
+ @page.page_translations.find_or_initialize_by(locale: @translation_locale)
58
+ @parent_options = parent_options_for(@page)
59
+ end
60
+
61
+ def create
62
+ @page = current_site.pages.build(page_params)
63
+ @translation_locale = requested_locale
64
+ purge_media_if_requested(@page)
65
+
66
+ if @page.save
67
+ redirect_to admin_pages_path, notice: t("cms.notices.page_created")
68
+ else
69
+ @parent_options = parent_options_for(nil)
70
+ render :new, status: :unprocessable_content
71
+ end
72
+ end
73
+
74
+ def update
75
+ @translation_locale = requested_locale
76
+ @page.assign_attributes(page_params)
77
+ purge_media_if_requested(@page)
78
+
79
+ if @page.save
80
+ redirect_to admin_pages_path, notice: t("cms.notices.page_updated")
81
+ else
82
+ @parent_options = parent_options_for(@page)
83
+ render :edit, status: :unprocessable_content
84
+ end
85
+ end
86
+
87
+ def destroy
88
+ @page.discard!
89
+ redirect_to admin_pages_path, notice: t("cms.notices.page_deleted")
90
+ end
91
+
92
+ private
93
+
94
+ def set_page
95
+ @page = current_site.pages.kept.includes(
96
+ :parent,
97
+ :page_translations,
98
+ { subpages: :localised },
99
+ { page_sections: :section },
100
+ { hero_image_attachment: :blob },
101
+ { media_files_attachments: :blob }
102
+ ).find(params[:id])
103
+ end
104
+
105
+ def page_params
106
+ params.require(:page).permit(
107
+ :slug,
108
+ :parent_id,
109
+ :position,
110
+ :home,
111
+ :template_key,
112
+ :status,
113
+ :show_in_header,
114
+ :show_in_footer,
115
+ :nav_group,
116
+ :nav_order,
117
+ :footer_order,
118
+ :hero_image,
119
+ media_files: [],
120
+ page_translations_attributes: %i[id locale title seo_title seo_description]
121
+ )
122
+ end
123
+
124
+ def requested_locale
125
+ params[:locale].presence || current_site.default_locale.presence || I18n.locale.to_s
126
+ end
127
+
128
+ def purge_media_if_requested(page)
129
+ page.hero_image.purge_later if params.dig(:page, :remove_hero_image) == "1"
130
+ page.media_files.purge if params.dig(:page, :remove_media_files) == "1"
131
+ end
132
+
133
+ def parent_options_for(current_page)
134
+ pages = current_site.pages.kept.ordered.includes(:localised).to_a
135
+ depths = page_depths(pages)
136
+ excluded_ids = current_page ? [current_page.id] + descendant_ids_for(pages, current_page.id) : []
137
+
138
+ pages.reject { |page| excluded_ids.include?(page.id) }.map do |p|
139
+ ["#{'— ' * depths.fetch(p.id, 0)}#{p.display_title}", p.id]
140
+ end
141
+ end
142
+
143
+ def descendant_ids_for(pages, page_id)
144
+ children_by_parent = pages.group_by(&:parent_id)
145
+ queue = Array(children_by_parent[page_id])
146
+ ids = []
147
+
148
+ until queue.empty?
149
+ page = queue.shift
150
+ ids << page.id
151
+ queue.concat(Array(children_by_parent[page.id]))
152
+ end
153
+
154
+ ids
155
+ end
156
+
157
+ def flattened_page_rows(pages)
158
+ children_by_parent = pages.group_by(&:parent_id)
159
+ rows = []
160
+
161
+ append_page_rows(rows, children_by_parent, children_by_parent[nil], 0)
162
+ rows
163
+ end
164
+
165
+ def append_page_rows(rows, children_by_parent, pages, depth)
166
+ Array(pages).each do |page|
167
+ rows << [page, depth]
168
+ append_page_rows(rows, children_by_parent, children_by_parent[page.id], depth + 1)
169
+ end
170
+ end
171
+
172
+ def page_depths(pages)
173
+ pages.to_h { |page| [page.id, page.depth] }
174
+ end
175
+
176
+ def matching_page_rows(pages, depths)
177
+ pages_by_id = pages.index_by(&:id)
178
+
179
+ current_site.pages.kept.search(params[:q]).pluck(:id).filter_map do |page_id|
180
+ page = pages_by_id[page_id]
181
+ next unless page
182
+
183
+ [page, depths.fetch(page.id, 0)]
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cms
4
+ module Admin
5
+ class SectionsController < BaseController
6
+ include PageScopedSections
7
+
8
+ before_action :set_page, if: :page_scoped_request?
9
+ before_action :set_section, only: %i[show edit update destroy]
10
+
11
+ def index
12
+ @sections = current_site.sections.kept.global.includes(:translations, :section_images).ordered
13
+ end
14
+
15
+ def show
16
+ @translation_locale = requested_locale
17
+ end
18
+
19
+ def new
20
+ @section = current_site.sections.build(kind: params[:kind].presence || "rich_text", global: !@page)
21
+ @translation_locale = requested_locale
22
+ @section.build_missing_locale_translations
23
+ end
24
+
25
+ def edit
26
+ @translation_locale = requested_locale
27
+ @section.build_missing_locale_translations
28
+ end
29
+
30
+ def create
31
+ @section = current_site.sections.build(section_params)
32
+ @section.global = !@page
33
+ @translation_locale = requested_locale
34
+
35
+ if @section.save
36
+ sync_section_images(@section)
37
+ if turbo_page_scoped_request?
38
+ attach_to_page!
39
+ flash.now[:notice] = success_notice_for(:create)
40
+ load_page_show_context
41
+ render :page_update
42
+ else
43
+ attach_to_page! if @page
44
+ redirect_to after_save_path, notice: success_notice_for(:create)
45
+ end
46
+ else
47
+ render :new, status: :unprocessable_content
48
+ end
49
+ end
50
+
51
+ def update
52
+ @translation_locale = requested_locale
53
+ @section.assign_attributes(section_params)
54
+
55
+ if @section.save
56
+ sync_section_images(@section)
57
+ if turbo_page_scoped_request?
58
+ flash.now[:notice] = success_notice_for(:update)
59
+ load_page_show_context
60
+ render :page_update
61
+ else
62
+ redirect_to after_save_path, notice: success_notice_for(:update)
63
+ end
64
+ else
65
+ render :edit, status: :unprocessable_content
66
+ end
67
+ end
68
+
69
+ def destroy
70
+ if turbo_page_scoped_request?
71
+ @page.page_sections.find_by!(section_id: @section.id).destroy
72
+ flash.now[:notice] = success_notice_for(:destroy)
73
+ load_page_show_context
74
+ render :page_update
75
+ elsif @page
76
+ @page.page_sections.find_by!(section_id: @section.id).destroy
77
+ else
78
+ @section.discard!
79
+ end
80
+
81
+ redirect_to after_destroy_path, notice: success_notice_for(:destroy) unless performed?
82
+ end
83
+
84
+ def sort
85
+ ids = params[:page_section_ids].to_a.map(&:to_i)
86
+ ids.each_with_index do |id, index|
87
+ @page.page_sections.find(id).update!(position: index)
88
+ end
89
+ head :ok
90
+ end
91
+
92
+ def attach
93
+ section = current_site.sections.global.find(params[:section_id])
94
+ @page.page_sections.find_or_create_by!(section: section) do |page_section|
95
+ page_section.position = next_position
96
+ end
97
+
98
+ if turbo_page_scoped_request?
99
+ flash.now[:notice] = t("cms.notices.section_attached")
100
+ load_page_show_context
101
+ render :page_update
102
+ else
103
+ redirect_to admin_page_path(@page), notice: t("cms.notices.section_attached")
104
+ end
105
+ end
106
+
107
+ private
108
+
109
+ def set_section
110
+ @section = current_site.sections.kept.includes(:translations).find(params[:id])
111
+ end
112
+
113
+ def requested_locale
114
+ params[:locale].presence ||
115
+ section_locale.presence ||
116
+ current_site.default_locale.presence ||
117
+ I18n.locale.to_s
118
+ end
119
+
120
+ def section_locale
121
+ return unless @section
122
+
123
+ @section.available_locales.first
124
+ end
125
+
126
+ def section_kind
127
+ params.dig(:section, :kind).presence || @section&.kind || params[:kind].presence || "rich_text"
128
+ end
129
+
130
+ def section_params
131
+ permitted = %i[global enabled]
132
+ permitted.unshift(:kind) unless @section&.persisted?
133
+ permitted += permitted_settings_keys
134
+ permitted << { translations_attributes: %i[id locale title subtitle content] }
135
+
136
+ params.require(:section).permit(*permitted)
137
+ end
138
+
139
+ def permitted_settings_keys
140
+ case section_kind
141
+ when "hero" then %i[background_color cta_url]
142
+ when "cta" then %i[button_url alignment]
143
+ else []
144
+ end
145
+ end
146
+
147
+ def sync_section_images(section)
148
+ return unless section_kind == "image"
149
+
150
+ ids = params.dig(:section, :image_ids).to_a.compact_blank.map(&:to_i)
151
+ section.section_images.destroy_all
152
+ ids.each_with_index do |image_id, index|
153
+ section.section_images.create!(image_id: image_id, position: index)
154
+ end
155
+ end
156
+
157
+ def after_save_path
158
+ @page ? admin_page_path(@page, locale: @translation_locale) : admin_sections_path(locale: @translation_locale)
159
+ end
160
+
161
+ def after_destroy_path
162
+ @page ? admin_page_path(@page, locale: params[:locale]) : admin_sections_path(locale: params[:locale])
163
+ end
164
+
165
+ def success_notice_for(action)
166
+ return t("cms.notices.section_added") if action == :create && @page
167
+ return t("cms.notices.section_removed") if action == :destroy && @page
168
+
169
+ {
170
+ create: t("cms.notices.section_created"),
171
+ update: t("cms.notices.section_updated"),
172
+ destroy: t("cms.notices.section_deleted")
173
+ }.fetch(action)
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cms
4
+ module Admin
5
+ class SitesController < BaseController
6
+ before_action :redirect_bootstrap_requests_when_site_available, only: %i[new create]
7
+
8
+ def new
9
+ @site = Cms::Site.new(default_locale: I18n.default_locale.to_s, published: true)
10
+ end
11
+
12
+ def create
13
+ @site = Cms::Site.new(site_params)
14
+ purge_media_if_requested(@site)
15
+
16
+ if @site.save
17
+ redirect_to admin_site_path, notice: t("cms.notices.site_created")
18
+ else
19
+ render :new, status: :unprocessable_content
20
+ end
21
+ end
22
+
23
+ def show
24
+ @site = current_site
25
+ end
26
+
27
+ def edit
28
+ @site = current_site
29
+ end
30
+
31
+ def update
32
+ @site = current_site
33
+ purge_media_if_requested(@site)
34
+
35
+ if @site.update(site_params)
36
+ redirect_to admin_site_path, notice: t("cms.notices.site_updated")
37
+ else
38
+ render :edit, status: :unprocessable_content
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def site_params
45
+ params.require(:site).permit(:name, :slug, :published, :default_locale, :logo)
46
+ end
47
+
48
+ def purge_media_if_requested(site)
49
+ site.logo.purge_later if params.dig(:site, :remove_logo) == "1"
50
+ end
51
+
52
+ def redirect_bootstrap_requests_when_site_available
53
+ return unless Cms::Site.exists?
54
+ return redirect_to(edit_admin_site_path) if configured_current_site.present?
55
+
56
+ raise_missing_current_site!
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cms
4
+ module Admin
5
+ class WebhookDeliveriesController < BaseController
6
+ before_action :set_webhook
7
+
8
+ def index
9
+ @deliveries = @webhook.deliveries.recent
10
+ end
11
+
12
+ private
13
+
14
+ def set_webhook
15
+ @webhook = current_site.webhooks.find(params[:webhook_id])
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cms
4
+ module Admin
5
+ class WebhooksController < BaseController
6
+ before_action :set_webhook, only: %i[edit update destroy]
7
+
8
+ def index
9
+ @webhooks = current_site.webhooks.order(created_at: :desc)
10
+ end
11
+
12
+ def new
13
+ @webhook = current_site.webhooks.build
14
+ end
15
+
16
+ def create
17
+ @webhook = current_site.webhooks.build(webhook_params)
18
+ if @webhook.save
19
+ redirect_to admin_webhooks_path, notice: t("cms.notices.webhook_created")
20
+ else
21
+ render :new, status: :unprocessable_content
22
+ end
23
+ end
24
+
25
+ def edit; end
26
+
27
+ def update
28
+ if @webhook.update(webhook_params)
29
+ redirect_to admin_webhooks_path, notice: t("cms.notices.webhook_updated")
30
+ else
31
+ render :edit, status: :unprocessable_content
32
+ end
33
+ end
34
+
35
+ def destroy
36
+ @webhook.destroy
37
+ redirect_to admin_webhooks_path, notice: t("cms.notices.webhook_deleted")
38
+ end
39
+
40
+ private
41
+
42
+ def set_webhook
43
+ @webhook = current_site.webhooks.find(params[:id])
44
+ end
45
+
46
+ def webhook_params
47
+ params.require(:webhook).permit(:url, :secret, :active, events: [])
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cms
4
+ module Api
5
+ class BaseController < ApplicationController
6
+ include Cms::SiteResolvable
7
+
8
+ layout false
9
+
10
+ rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_json
11
+
12
+ private
13
+
14
+ def render_not_found_json
15
+ render json: { error: t("cms.errors.api.page_not_found") }, status: :not_found
16
+ end
17
+
18
+ def api_site_serializer(site, resolved_locale:)
19
+ Cms.api_site_serializer_class.new(
20
+ site: site,
21
+ requested_locale: resolved_locale,
22
+ main_app: main_app
23
+ )
24
+ end
25
+
26
+ def api_page_serializer(site, page, resolved_locale:)
27
+ Cms.api_page_serializer_class.new(
28
+ site: site,
29
+ page: page,
30
+ requested_locale: resolved_locale,
31
+ main_app: main_app
32
+ )
33
+ end
34
+
35
+ def truthy_param?(value)
36
+ ActiveModel::Type::Boolean.new.cast(value)
37
+ end
38
+
39
+ def request_locale
40
+ params[:locale].presence ||
41
+ request.headers["Accept-Language"].to_s.split(",").first&.strip
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cms
4
+ module Api
5
+ module V1
6
+ class BaseController < Cms::Api::BaseController
7
+ before_action :authenticate_api_key!
8
+
9
+ private
10
+
11
+ def authenticate_api_key!
12
+ site = configured_current_site || Cms::Site.live.find_by(slug: resolved_site_slug!)
13
+ return render json: { error: t("cms.errors.api.site_not_found") }, status: :not_found unless site
14
+
15
+ token = request.headers["Authorization"].to_s.sub(/\ABearer\s+/, "")
16
+ @api_key = Cms::ApiKey.active.find_by(token: token, site: site)
17
+
18
+ unless @api_key
19
+ render json: { error: t("cms.errors.api.unauthorized") }, status: :unauthorized
20
+ return
21
+ end
22
+
23
+ @resolved_site = site
24
+ @api_key.touch_last_used!
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cms
4
+ module Api
5
+ module V1
6
+ class PagesController < BaseController
7
+ def show
8
+ site = find_site!
9
+ result = page_resolver_class.resolve(site: site, slug: params[:slug], locale: request_locale)
10
+
11
+ return render json: { error: t("cms.errors.api.page_not_found") }, status: :not_found unless result
12
+
13
+ I18n.with_locale(result.locale) do
14
+ serializer = api_page_serializer(site, result.page, resolved_locale: result.locale)
15
+ render json: serializer.as_json(include_site: truthy_param?(params[:include_site]))
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cms
4
+ module Api
5
+ module V1
6
+ class SitesController < BaseController
7
+ def show
8
+ site = find_site!
9
+ resolved_locale = request_locale.presence || site.default_locale
10
+
11
+ I18n.with_locale(resolved_locale) do
12
+ render json: api_site_serializer(site, resolved_locale: resolved_locale).as_json
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cms
4
+ class ApplicationController < Cms.parent_controller.constantize
5
+ private
6
+
7
+ def page_resolver_class
8
+ Cms.page_resolver_class
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cms
4
+ module Public
5
+ class BaseController < ApplicationController
6
+ layout -> { Cms.config.public_layout }
7
+
8
+ helper Cms::ApplicationHelper
9
+ helper Cms::MediaHelper
10
+ helper Cms::SitesHelper
11
+ helper Cms::SectionsHelper
12
+ helper Cms::PagesHelper
13
+
14
+ rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
15
+
16
+ private
17
+
18
+ def render_not_found
19
+ render plain: t("cms.errors.page_not_found"), status: :not_found
20
+ end
21
+ end
22
+ end
23
+ end