ruby_cms 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.
- checksums.yaml +7 -0
- data/.cursor/dhh.mdc +698 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/README.md +235 -0
- data/Rakefile +30 -0
- data/app/components/ruby_cms/admin/admin_page/admin_table_content.rb +32 -0
- data/app/components/ruby_cms/admin/admin_page.rb +345 -0
- data/app/components/ruby_cms/admin/base_component.rb +78 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table.rb +149 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_actions.rb +127 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_body.rb +15 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_cell.rb +41 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_head.rb +33 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_delete_modal.rb +174 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header.rb +59 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header_bar.rb +159 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_pagination.rb +192 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_row.rb +97 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +137 -0
- data/app/controllers/concerns/ruby_cms/admin_pagination.rb +120 -0
- data/app/controllers/concerns/ruby_cms/admin_turbo_table.rb +68 -0
- data/app/controllers/concerns/ruby_cms/page_tracking.rb +52 -0
- data/app/controllers/concerns/ruby_cms/visitor_error_capture.rb +39 -0
- data/app/controllers/ruby_cms/admin/analytics_controller.rb +191 -0
- data/app/controllers/ruby_cms/admin/base_controller.rb +105 -0
- data/app/controllers/ruby_cms/admin/content_blocks_controller.rb +390 -0
- data/app/controllers/ruby_cms/admin/dashboard_controller.rb +50 -0
- data/app/controllers/ruby_cms/admin/locale_controller.rb +20 -0
- data/app/controllers/ruby_cms/admin/permissions_controller.rb +66 -0
- data/app/controllers/ruby_cms/admin/settings_controller.rb +223 -0
- data/app/controllers/ruby_cms/admin/user_permissions_controller.rb +59 -0
- data/app/controllers/ruby_cms/admin/users_controller.rb +107 -0
- data/app/controllers/ruby_cms/admin/visitor_errors_controller.rb +89 -0
- data/app/controllers/ruby_cms/admin/visual_editor_controller.rb +322 -0
- data/app/controllers/ruby_cms/errors_controller.rb +35 -0
- data/app/helpers/ruby_cms/admin/admin_page_helper.rb +21 -0
- data/app/helpers/ruby_cms/admin/bulk_action_table_helper.rb +159 -0
- data/app/helpers/ruby_cms/application_helper.rb +41 -0
- data/app/helpers/ruby_cms/bulk_action_table_helper.rb +151 -0
- data/app/helpers/ruby_cms/content_blocks_helper.rb +375 -0
- data/app/helpers/ruby_cms/settings_helper.rb +160 -0
- data/app/javascript/controllers/ruby_cms/auto_save_preference_controller.js +73 -0
- data/app/javascript/controllers/ruby_cms/bulk_action_table_controller.js +553 -0
- data/app/javascript/controllers/ruby_cms/clickable_row_controller.js +28 -0
- data/app/javascript/controllers/ruby_cms/flash_messages_controller.js +29 -0
- data/app/javascript/controllers/ruby_cms/index.js +104 -0
- data/app/javascript/controllers/ruby_cms/locale_tabs_controller.js +34 -0
- data/app/javascript/controllers/ruby_cms/mobile_menu_controller.js +55 -0
- data/app/javascript/controllers/ruby_cms/nav_order_sortable_controller.js +192 -0
- data/app/javascript/controllers/ruby_cms/page_preview_controller.js +135 -0
- data/app/javascript/controllers/ruby_cms/toggle_controller.js +39 -0
- data/app/javascript/controllers/ruby_cms/visual_editor_controller.js +321 -0
- data/app/models/concerns/content_block/publishable.rb +54 -0
- data/app/models/concerns/content_block/searchable.rb +22 -0
- data/app/models/content_block.rb +155 -0
- data/app/models/ruby_cms/content_block.rb +8 -0
- data/app/models/ruby_cms/permission.rb +28 -0
- data/app/models/ruby_cms/permittable.rb +39 -0
- data/app/models/ruby_cms/preference.rb +111 -0
- data/app/models/ruby_cms/user_permission.rb +12 -0
- data/app/models/ruby_cms/visitor_error.rb +109 -0
- data/app/services/ruby_cms/analytics/report.rb +362 -0
- data/app/services/ruby_cms/security_tracker.rb +92 -0
- data/app/views/layouts/ruby_cms/_admin_flash_messages.html.erb +37 -0
- data/app/views/layouts/ruby_cms/_admin_sidebar.html.erb +121 -0
- data/app/views/layouts/ruby_cms/admin.html.erb +81 -0
- data/app/views/layouts/ruby_cms/minimal.html.erb +181 -0
- data/app/views/ruby_cms/admin/analytics/index.html.erb +160 -0
- data/app/views/ruby_cms/admin/analytics/page_details.html.erb +84 -0
- data/app/views/ruby_cms/admin/analytics/partials/_back_button.html.erb +3 -0
- data/app/views/ruby_cms/admin/analytics/partials/_browser_device.html.erb +40 -0
- data/app/views/ruby_cms/admin/analytics/partials/_daily_activity_chart.html.erb +58 -0
- data/app/views/ruby_cms/admin/analytics/partials/_hourly_activity_chart.html.erb +51 -0
- data/app/views/ruby_cms/admin/analytics/partials/_recent_activity.html.erb +31 -0
- data/app/views/ruby_cms/admin/analytics/partials/_security_alert.html.erb +4 -0
- data/app/views/ruby_cms/admin/analytics/partials/_top_referrers.html.erb +21 -0
- data/app/views/ruby_cms/admin/analytics/visitor_details.html.erb +125 -0
- data/app/views/ruby_cms/admin/content_blocks/_form.html.erb +161 -0
- data/app/views/ruby_cms/admin/content_blocks/_row.html.erb +25 -0
- data/app/views/ruby_cms/admin/content_blocks/edit.html.erb +17 -0
- data/app/views/ruby_cms/admin/content_blocks/index.html.erb +66 -0
- data/app/views/ruby_cms/admin/content_blocks/new.html.erb +5 -0
- data/app/views/ruby_cms/admin/content_blocks/show.html.erb +110 -0
- data/app/views/ruby_cms/admin/dashboard/index.html.erb +198 -0
- data/app/views/ruby_cms/admin/permissions/_row.html.erb +11 -0
- data/app/views/ruby_cms/admin/permissions/index.html.erb +62 -0
- data/app/views/ruby_cms/admin/settings/index.html.erb +220 -0
- data/app/views/ruby_cms/admin/shared/_bulk_action_table_index.html.erb +56 -0
- data/app/views/ruby_cms/admin/user_permissions/index.html.erb +55 -0
- data/app/views/ruby_cms/admin/users/_row.html.erb +14 -0
- data/app/views/ruby_cms/admin/users/index.html.erb +70 -0
- data/app/views/ruby_cms/admin/visitor_errors/_row.html.erb +35 -0
- data/app/views/ruby_cms/admin/visitor_errors/index.html.erb +57 -0
- data/app/views/ruby_cms/admin/visitor_errors/show.html.erb +147 -0
- data/app/views/ruby_cms/admin/visual_editor/index.html.erb +144 -0
- data/app/views/ruby_cms/errors/not_found.html.erb +92 -0
- data/config/database.yml +6 -0
- data/config/importmap.rb +36 -0
- data/config/locales/en.yml +101 -0
- data/config/routes.rb +65 -0
- data/db/migrate/20260125000001_create_ruby_cms_permissions.rb +14 -0
- data/db/migrate/20260125000002_create_ruby_cms_user_permissions.rb +14 -0
- data/db/migrate/20260125000003_create_ruby_cms_content_blocks.rb +19 -0
- data/db/migrate/20260125000010_add_indexes_to_ruby_cms_tables.rb +9 -0
- data/db/migrate/20260127000001_add_locale_to_ruby_cms_content_blocks.rb +34 -0
- data/db/migrate/20260129000001_create_ruby_cms_visitor_errors.rb +24 -0
- data/db/migrate/20260130000001_add_referer_and_query_to_ruby_cms_visitor_errors.rb +8 -0
- data/db/migrate/20260130000002_create_ruby_cms_preferences.rb +16 -0
- data/db/migrate/20260130000003_add_category_to_ruby_cms_preferences.rb +8 -0
- data/db/migrate/20260211000001_add_ruby_cms_analytics_fields_to_ahoy_events.rb +19 -0
- data/db/migrate/20260212000001_use_unprefixed_cms_tables.rb +146 -0
- data/exe/ruby_cms +25 -0
- data/lib/generators/ruby_cms/install_generator.rb +1062 -0
- data/lib/generators/ruby_cms/templates/admin.html.erb +82 -0
- data/lib/generators/ruby_cms/templates/ruby_cms.rb +86 -0
- data/lib/ruby_cms/app_integration.rb +82 -0
- data/lib/ruby_cms/cli.rb +169 -0
- data/lib/ruby_cms/content_blocks_grouping.rb +41 -0
- data/lib/ruby_cms/content_blocks_sync.rb +329 -0
- data/lib/ruby_cms/css_compiler.rb +35 -0
- data/lib/ruby_cms/engine.rb +498 -0
- data/lib/ruby_cms/settings.rb +145 -0
- data/lib/ruby_cms/settings_registry.rb +289 -0
- data/lib/ruby_cms/version.rb +5 -0
- data/lib/ruby_cms.rb +195 -0
- data/lib/tasks/ruby_cms.rake +27 -0
- data/log/test.log +17875 -0
- data/sig/ruby_cms.rbs +4 -0
- metadata +223 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<%# Single admin layout for app and RubyCMS. Uses RubyCMS sidebar, flash, content_for. %>
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html lang="<%= I18n.locale %>">
|
|
4
|
+
<head>
|
|
5
|
+
<title><%= content_for(:title) || "Admin Panel" %></title>
|
|
6
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
7
|
+
<meta name="rails-env" content="<%= Rails.env %>">
|
|
8
|
+
<%= csrf_meta_tags %>
|
|
9
|
+
<%= csp_meta_tag %>
|
|
10
|
+
|
|
11
|
+
<%= favicon_link_tag asset_path("favicon.ico") rescue nil %>
|
|
12
|
+
<link rel="icon" type="image/png" sizes="32x32" href="<%= asset_path("favicon-32x32.png") %>">
|
|
13
|
+
<link rel="icon" type="image/png" sizes="16x16" href="<%= asset_path("favicon-16x16.png") %>">
|
|
14
|
+
<link rel="apple-touch-icon" href="<%= asset_path("apple-touch-icon.png") %>">
|
|
15
|
+
<link rel="manifest" href="<%= asset_path("site.webmanifest") %>">
|
|
16
|
+
|
|
17
|
+
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
|
18
|
+
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
|
|
19
|
+
<%= stylesheet_link_tag "actiontext", "data-turbo-track": "reload" rescue nil %>
|
|
20
|
+
<%= javascript_importmap_tags "admin" %>
|
|
21
|
+
</head>
|
|
22
|
+
<body class="min-h-screen bg-gray-50 text-gray-900">
|
|
23
|
+
<div class="flex h-screen overflow-hidden" data-controller="ruby-cms--mobile-menu">
|
|
24
|
+
<%# Mobile menu toggle (visible on md and below) %>
|
|
25
|
+
<button type="button" class="md:hidden fixed top-4 left-4 z-50 inline-flex items-center justify-center p-2 bg-white border border-gray-200/80 rounded-lg shadow-sm cursor-pointer" aria-label="Toggle menu" data-action="click->ruby-cms--mobile-menu#toggle">
|
|
26
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
27
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
|
28
|
+
</svg>
|
|
29
|
+
</button>
|
|
30
|
+
|
|
31
|
+
<%= render "layouts/ruby_cms/admin_sidebar" %>
|
|
32
|
+
|
|
33
|
+
<%# Mobile overlay (JS adds class "show") %>
|
|
34
|
+
<div class="hidden fixed inset-0 bg-black/50 z-40 lg:hidden" data-ruby-cms--mobile-menu-target="overlay" data-action="click->ruby-cms--mobile-menu#close"></div>
|
|
35
|
+
|
|
36
|
+
<main class="flex-1 flex flex-col overflow-hidden bg-gray-50 min-w-0" role="main">
|
|
37
|
+
<% if content_for?(:full_width) %>
|
|
38
|
+
<%= yield %>
|
|
39
|
+
<% else %>
|
|
40
|
+
<div class="flex-1 flex flex-col min-h-0 p-6 overflow-y-auto bg-gray-50">
|
|
41
|
+
<div class="mx-auto w-full max-w-7xl">
|
|
42
|
+
<% if content_for?(:title) || content_for?(:header_actions) %>
|
|
43
|
+
<header class="flex-shrink-0 mb-4 <%= yield(:header_styles) if content_for?(:header_styles) %>">
|
|
44
|
+
<div class="flex flex-wrap items-center justify-between gap-4">
|
|
45
|
+
<div>
|
|
46
|
+
<% if content_for?(:title) %>
|
|
47
|
+
<h1 class="text-lg font-semibold tracking-tight text-gray-900"><%= yield :title %></h1>
|
|
48
|
+
<% end %>
|
|
49
|
+
<% if content_for?(:breadcrumbs) %>
|
|
50
|
+
<nav class="mt-0.5 text-sm text-gray-500" aria-label="Breadcrumb">
|
|
51
|
+
<ol class="flex items-center flex-wrap gap-x-2 gap-y-1">
|
|
52
|
+
<%= yield :breadcrumbs %>
|
|
53
|
+
</ol>
|
|
54
|
+
</nav>
|
|
55
|
+
<% end %>
|
|
56
|
+
</div>
|
|
57
|
+
<% if content_for?(:header_actions) %>
|
|
58
|
+
<div class="flex items-center flex-shrink-0">
|
|
59
|
+
<%= yield :header_actions %>
|
|
60
|
+
</div>
|
|
61
|
+
<% end %>
|
|
62
|
+
</div>
|
|
63
|
+
</header>
|
|
64
|
+
<% elsif content_for?(:breadcrumbs) %>
|
|
65
|
+
<nav class="mb-4" aria-label="Breadcrumb">
|
|
66
|
+
<ol class="flex items-center space-x-2 text-sm text-gray-600">
|
|
67
|
+
<%= content_for :breadcrumbs %>
|
|
68
|
+
</ol>
|
|
69
|
+
</nav>
|
|
70
|
+
<% end %>
|
|
71
|
+
|
|
72
|
+
<%= render "layouts/ruby_cms/admin_flash_messages" %>
|
|
73
|
+
|
|
74
|
+
<%= yield %>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<% end %>
|
|
78
|
+
</main>
|
|
79
|
+
</div>
|
|
80
|
+
</body>
|
|
81
|
+
</html>
|
|
82
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# RubyCMS configuration
|
|
4
|
+
|
|
5
|
+
# -----------------------------------------------------------------------------
|
|
6
|
+
# Session / cookie hardening (recommended for /admin)
|
|
7
|
+
# Apply in config/initializers/session_store.rb or your session config:
|
|
8
|
+
#
|
|
9
|
+
# Rails.application.config.session_store :cookie_store,
|
|
10
|
+
# key: "_session",
|
|
11
|
+
# httponly: true,
|
|
12
|
+
# same_site: :lax, # or :strict
|
|
13
|
+
# secure: Rails.env.production?,
|
|
14
|
+
# expire_after: 2.weeks
|
|
15
|
+
#
|
|
16
|
+
# In your auth flow: rotate session on sign-in (e.g. Session.find_by(...)&.destroy
|
|
17
|
+
# before creating a new one). Use safe return-to: validate redirect paths to same
|
|
18
|
+
# origin or allowlist; avoid open redirects.
|
|
19
|
+
# -----------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
# -----------------------------------------------------------------------------
|
|
22
|
+
# CSP (Content Security Policy): set sensible defaults; document overrides.
|
|
23
|
+
# Example in config/initializers/content_security_policy.rb:
|
|
24
|
+
#
|
|
25
|
+
# Rails.application.config.content_security_policy do |p|
|
|
26
|
+
# p.default_src :self; p.script_src :self; p.style_src :self, :unsafe_inline
|
|
27
|
+
# end
|
|
28
|
+
# -----------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
RubyCms.configure do |c|
|
|
31
|
+
# Controller from which all /admin controllers inherit.
|
|
32
|
+
# Must provide current_user and run require_authentication.
|
|
33
|
+
# c.admin_base_controller = "ApplicationController"
|
|
34
|
+
|
|
35
|
+
# User model class name for permissions. Change if your app uses a different name.
|
|
36
|
+
# c.user_class_name = "User"
|
|
37
|
+
|
|
38
|
+
# When no Permission records exist, allow user.admin? to access /admin (bootstrap).
|
|
39
|
+
# Set to false to require permissions from the start.
|
|
40
|
+
# c.bootstrap_admin_with_role = true
|
|
41
|
+
|
|
42
|
+
# Redirect path when unauthenticated or not permitted (default: "/").
|
|
43
|
+
# Use "/session/new" to send to sign-in.
|
|
44
|
+
# c.unauthorized_redirect_path = "/"
|
|
45
|
+
|
|
46
|
+
# Visual editor: allowlist of page_key => template path.
|
|
47
|
+
# c.preview_templates = { "home" => "pages/home", "about" => "pages/about" }
|
|
48
|
+
|
|
49
|
+
# Preview data proc to pass instance variables.
|
|
50
|
+
# c.preview_data = ->(page_key, view) { { products: Product.limit(5) } }
|
|
51
|
+
|
|
52
|
+
# Optional hook: customize Ahoy visit scope (e.g. exclude internal traffic)
|
|
53
|
+
# c.analytics_visit_scope = ->(scope) { scope.where.not(ip: ["127.0.0.1"]) }
|
|
54
|
+
|
|
55
|
+
# Optional hook: customize Ahoy event scope
|
|
56
|
+
# c.analytics_event_scope = ->(scope) { scope }
|
|
57
|
+
|
|
58
|
+
# Optional hook: provide extra dashboard cards
|
|
59
|
+
# c.analytics_extra_cards = lambda do |start_date:, end_date:, period:, visits_scope:, events_scope:|
|
|
60
|
+
# [{ title: "Custom KPI", value: visits_scope.where.not(utm_source: nil).count }]
|
|
61
|
+
# end
|
|
62
|
+
|
|
63
|
+
# -----------------------------------------------------------------------------
|
|
64
|
+
# Optional bootstrap values (initializer -> DB import, once)
|
|
65
|
+
#
|
|
66
|
+
# On first boot/install, RubyCMS imports matching keys from config.ruby_cms into
|
|
67
|
+
# ruby_cms_preferences. After that, DB settings are source of truth.
|
|
68
|
+
#
|
|
69
|
+
# Example bootstrap values:
|
|
70
|
+
# c.analytics_default_period = "week"
|
|
71
|
+
# c.analytics_max_date_range_days = 365
|
|
72
|
+
# c.analytics_cache_duration_seconds = 600
|
|
73
|
+
# c.analytics_max_popular_pages = 10
|
|
74
|
+
# c.analytics_max_top_visitors = 10
|
|
75
|
+
# c.analytics_high_volume_threshold = 1000
|
|
76
|
+
# c.analytics_rapid_request_threshold = 50
|
|
77
|
+
# c.pagination_min_per_page = 5
|
|
78
|
+
# c.pagination_max_per_page = 200
|
|
79
|
+
# c.reserved_key_prefixes = %w[admin_]
|
|
80
|
+
# c.image_content_types = %w[image/png image/jpeg image/gif image/webp]
|
|
81
|
+
# c.image_max_size = 5 * 1024 * 1024
|
|
82
|
+
#
|
|
83
|
+
# To re-run manually:
|
|
84
|
+
# rails ruby_cms:import_initializer_settings
|
|
85
|
+
# -----------------------------------------------------------------------------
|
|
86
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
# App Integration: Connect CMS to host app features
|
|
5
|
+
module AppIntegration
|
|
6
|
+
# Link App: Connect CMS pages to app routes/controllers
|
|
7
|
+
module LinkApp
|
|
8
|
+
# Register an app route that can be linked from CMS
|
|
9
|
+
# @param key [String] Unique identifier for the route
|
|
10
|
+
# @param route_name [String] Route helper name (e.g. "products_path", "user_dashboard_path")
|
|
11
|
+
# @param label [String] Display label
|
|
12
|
+
# @param description [String] Optional description
|
|
13
|
+
# @param params [Hash] Default route parameters
|
|
14
|
+
def self.register_route(key:, route_name:, label:, description: nil, params: {})
|
|
15
|
+
@registered_routes ||= {}
|
|
16
|
+
@registered_routes[key] = {
|
|
17
|
+
route_name:,
|
|
18
|
+
label:,
|
|
19
|
+
description:,
|
|
20
|
+
params:
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Get all registered routes
|
|
25
|
+
def self.registered_routes
|
|
26
|
+
@registered_routes || {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Get a route by key
|
|
30
|
+
def self.get_route(key)
|
|
31
|
+
registered_routes[key.to_s]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# App Settings: Load app-specific settings/config into CMS context
|
|
36
|
+
module AppSettings
|
|
37
|
+
# Register a setting that can be loaded into CMS context
|
|
38
|
+
# @param key [String] Setting key
|
|
39
|
+
# @param loader [Proc] Callable that returns the setting value: ->(view_context) { ... }
|
|
40
|
+
def self.register_setting(key:, loader:)
|
|
41
|
+
@registered_settings ||= {}
|
|
42
|
+
@registered_settings[key.to_s] = loader
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get all settings for a view context
|
|
46
|
+
def self.load_settings(view_context)
|
|
47
|
+
(@registered_settings || {}).transform_values do |loader|
|
|
48
|
+
loader.call(view_context)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get a specific setting
|
|
53
|
+
def self.get_setting(key, view_context)
|
|
54
|
+
loader = (@registered_settings || {})[key.to_s]
|
|
55
|
+
loader&.call(view_context)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Convenience methods
|
|
61
|
+
class << self
|
|
62
|
+
# Register an app route
|
|
63
|
+
def register_app_route(**)
|
|
64
|
+
AppIntegration::LinkApp.register_route(**)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get registered routes
|
|
68
|
+
def app_routes
|
|
69
|
+
AppIntegration::LinkApp.registered_routes
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Register an app setting
|
|
73
|
+
def register_app_setting(**)
|
|
74
|
+
AppIntegration::AppSettings.register_setting(**)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Load app settings for view context
|
|
78
|
+
def load_app_settings(view_context)
|
|
79
|
+
AppIntegration::AppSettings.load_settings(view_context)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/ruby_cms/cli.rb
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module RubyCms
|
|
6
|
+
class CLI < Thor
|
|
7
|
+
default_task :setup_admin
|
|
8
|
+
|
|
9
|
+
desc "setup_admin",
|
|
10
|
+
"Interactively create or select the first admin user and grant full permissions"
|
|
11
|
+
def setup_admin
|
|
12
|
+
RunSetupAdmin.call(shell:)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def shell
|
|
18
|
+
@shell ||= Thor::Shell::Basic.new
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Logic for the interactive first-admin setup. Uses Thor::Shell for prompts.
|
|
23
|
+
class RunSetupAdmin
|
|
24
|
+
ADMIN_PERMISSION_KEYS = %w[
|
|
25
|
+
manage_admin manage_permissions manage_content_blocks manage_visitor_errors
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
def call(shell: Thor::Shell::Basic.new)
|
|
30
|
+
new(shell:).call
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(shell:)
|
|
35
|
+
@shell = shell
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def call
|
|
39
|
+
RubyCms::Permission.ensure_defaults!
|
|
40
|
+
user_class = Rails.application.config.ruby_cms.user_class_name.constantize
|
|
41
|
+
email_attr = user_class.column_names.include?("email_address") ? :email_address : :email
|
|
42
|
+
|
|
43
|
+
user = choose_or_create_user(user_class, email_attr)
|
|
44
|
+
return if user.nil?
|
|
45
|
+
|
|
46
|
+
grant_permissions(user)
|
|
47
|
+
notify_success(user, email_attr)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def notify_success(user, email_attr)
|
|
53
|
+
@shell.say(
|
|
54
|
+
"\nDone. #{user.public_send(email_attr)}
|
|
55
|
+
now has: #{ADMIN_PERMISSION_KEYS.join(', ')}.", :green
|
|
56
|
+
)
|
|
57
|
+
@shell.say("Visit /admin to sign in.", :green)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def choose_or_create_user(user_class, email_attr)
|
|
61
|
+
users = fetch_recent_users(user_class)
|
|
62
|
+
|
|
63
|
+
if users.any?
|
|
64
|
+
prompt_existing_users(users, email_attr)
|
|
65
|
+
else
|
|
66
|
+
@shell.say("\nNo users yet. Create the first admin user.")
|
|
67
|
+
create_user(user_class, email_attr)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def fetch_recent_users(user_class)
|
|
72
|
+
user_class.order(id: :asc).limit(20).to_a
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def prompt_existing_users(users, email_attr)
|
|
76
|
+
display_users(users, email_attr)
|
|
77
|
+
choice = ask_for_user_choice(users.size)
|
|
78
|
+
|
|
79
|
+
case choice
|
|
80
|
+
when "0"
|
|
81
|
+
find_or_create_by_email(users.first.class, email_attr)
|
|
82
|
+
else
|
|
83
|
+
select_user_or_fallback(users, choice.to_i, email_attr)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def display_users(users, email_attr)
|
|
88
|
+
@shell.say("\nExisting users:", :bold)
|
|
89
|
+
users.each_with_index do |u, i|
|
|
90
|
+
@shell.say(" #{i + 1}. #{u.public_send(email_attr)}")
|
|
91
|
+
end
|
|
92
|
+
@shell.say(" 0. Enter another email (or create new user)")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def ask_for_user_choice(max)
|
|
96
|
+
@shell.ask("Select (0–#{max}):", default: "1").to_s.strip
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def select_user_or_fallback(users, index, email_attr)
|
|
100
|
+
if index.between?(1, users.size)
|
|
101
|
+
users[index - 1]
|
|
102
|
+
else
|
|
103
|
+
find_or_create_by_email(users.first.class, email_attr)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def find_or_create_by_email(user_class, email_attr)
|
|
108
|
+
email = @shell.ask("Email:").to_s.strip
|
|
109
|
+
return nil if email.blank?
|
|
110
|
+
|
|
111
|
+
user = user_class.find_by(email_attr => email)
|
|
112
|
+
if user
|
|
113
|
+
user
|
|
114
|
+
else
|
|
115
|
+
@shell.say("User not found.")
|
|
116
|
+
create = @shell.yes?("Create new user with this email?", default: true)
|
|
117
|
+
create ? create_user_with_email(user_class, email_attr, email) : nil
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def create_user(user_class, email_attr)
|
|
122
|
+
email = @shell.ask("Email:").to_s.strip
|
|
123
|
+
return nil if email.blank?
|
|
124
|
+
|
|
125
|
+
create_user_with_email(user_class, email_attr, email)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def create_user_with_email(user_class, email_attr, email)
|
|
129
|
+
password, password_confirmation = ask_password_and_confirmation
|
|
130
|
+
return nil if password.blank? || password_confirmation.blank?
|
|
131
|
+
|
|
132
|
+
attrs = {
|
|
133
|
+
email_attr => email, password: password,
|
|
134
|
+
password_confirmation: password_confirmation
|
|
135
|
+
}
|
|
136
|
+
user_class.create!(attrs)
|
|
137
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
138
|
+
@shell.say("Could not create user: #{e.record.errors.full_messages.to_sentence}", :red)
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def ask_password_and_confirmation
|
|
143
|
+
password = ask_password("Password:")
|
|
144
|
+
return [nil, nil] if password.blank?
|
|
145
|
+
|
|
146
|
+
password_confirmation = ask_password("Password (again):")
|
|
147
|
+
return [password, password_confirmation] unless password != password_confirmation
|
|
148
|
+
|
|
149
|
+
@shell.say("Passwords did not match. Aborted.", :red)
|
|
150
|
+
[nil, nil]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def ask_password(prompt)
|
|
154
|
+
if $stdin.respond_to?(:noecho) && $stdin.tty?
|
|
155
|
+
@shell.say("#{prompt} (hidden)")
|
|
156
|
+
$stdin.noecho { $stdin.gets&.chomp }.to_s
|
|
157
|
+
else
|
|
158
|
+
@shell.ask("#{prompt}:").to_s.strip
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def grant_permissions(user)
|
|
163
|
+
ADMIN_PERMISSION_KEYS.each do |key|
|
|
164
|
+
perm = RubyCms::Permission.find_by!(key:)
|
|
165
|
+
RubyCms::UserPermission.find_or_create_by!(user: user, permission: perm)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
module ContentBlocksGrouping
|
|
5
|
+
Row = ::Struct.new(:key, :id, :locale, :title, :content_type, :published, :content_block) do
|
|
6
|
+
def published?
|
|
7
|
+
published
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def model_name # rubocop:disable Rails/Delegate
|
|
11
|
+
::ContentBlock.model_name
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# rubocop:disable Rails/Blank -- use plain Ruby for standalone unit specs without Rails
|
|
17
|
+
def group_by_key(collection)
|
|
18
|
+
return [] if collection.nil? || collection.empty?
|
|
19
|
+
|
|
20
|
+
collection
|
|
21
|
+
.group_by(&:key)
|
|
22
|
+
.map {|key, blocks| build_row(key, blocks) }
|
|
23
|
+
.sort_by(&:key)
|
|
24
|
+
end
|
|
25
|
+
# rubocop:enable Rails/Blank
|
|
26
|
+
|
|
27
|
+
def build_row(key, blocks)
|
|
28
|
+
first = blocks.first
|
|
29
|
+
Row.new(
|
|
30
|
+
key: key,
|
|
31
|
+
id: first.id,
|
|
32
|
+
locale: blocks.map(&:locale).sort.join(", "),
|
|
33
|
+
title: first.title,
|
|
34
|
+
content_type: first.content_type,
|
|
35
|
+
published: blocks.all?(&:published?),
|
|
36
|
+
content_block: first
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|