lean_cms 0.2.12
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/CHANGELOG.md +235 -0
- data/LICENSE +21 -0
- data/README.md +107 -0
- data/app/assets/images/lean_cms/sloth-404.png +0 -0
- data/app/assets/images/lean_cms/sloth-500.png +0 -0
- data/app/assets/images/lean_cms/sloth-favicon-16.png +0 -0
- data/app/assets/images/lean_cms/sloth-favicon-32.png +0 -0
- data/app/assets/images/lean_cms/sloth-favicon-64.png +0 -0
- data/app/assets/images/lean_cms/sloth-logo.png +0 -0
- data/app/assets/lean_cms/actiontext.css +440 -0
- data/app/assets/lean_cms/cms_edit_controls.css +548 -0
- data/app/assets/tailwind/lean_cms/engine.css +14 -0
- data/app/components/lean_cms/base_component.rb +61 -0
- data/app/components/lean_cms/bullets_section_component.html.erb +23 -0
- data/app/components/lean_cms/bullets_section_component.rb +54 -0
- data/app/components/lean_cms/cards_section_component.html.erb +237 -0
- data/app/components/lean_cms/cards_section_component.rb +71 -0
- data/app/components/lean_cms/editable_content_component.html.erb +15 -0
- data/app/components/lean_cms/editable_content_component.rb +53 -0
- data/app/components/lean_cms/section_component.html.erb +18 -0
- data/app/components/lean_cms/section_component.rb +35 -0
- data/app/controllers/concerns/lean_cms/authentication.rb +60 -0
- data/app/controllers/concerns/lean_cms/authorization.rb +60 -0
- data/app/controllers/lean_cms/activity_controller.rb +16 -0
- data/app/controllers/lean_cms/application_controller.rb +48 -0
- data/app/controllers/lean_cms/dashboard_controller.rb +13 -0
- data/app/controllers/lean_cms/form_submissions_controller.rb +37 -0
- data/app/controllers/lean_cms/notification_settings_controller.rb +145 -0
- data/app/controllers/lean_cms/notifications_controller.rb +26 -0
- data/app/controllers/lean_cms/page_contents_controller.rb +403 -0
- data/app/controllers/lean_cms/password_setup_controller.rb +65 -0
- data/app/controllers/lean_cms/passwords_controller.rb +42 -0
- data/app/controllers/lean_cms/posts_controller.rb +78 -0
- data/app/controllers/lean_cms/sessions_controller.rb +50 -0
- data/app/controllers/lean_cms/settings_controller.rb +124 -0
- data/app/controllers/lean_cms/users_controller.rb +113 -0
- data/app/helpers/lean_cms/activity_helper.rb +190 -0
- data/app/helpers/lean_cms/application_helper.rb +43 -0
- data/app/helpers/lean_cms/content_helper.rb +34 -0
- data/app/helpers/lean_cms/page_content_helper.rb +359 -0
- data/app/javascript/controllers/cards_editor_controller.js +317 -0
- data/app/javascript/controllers/cms_sticky_overlay_controller.js +59 -0
- data/app/javascript/controllers/field_editor_form_controller.js +68 -0
- data/app/javascript/controllers/field_editor_modal_controller.js +79 -0
- data/app/javascript/controllers/inline_edit_controller.js +414 -0
- data/app/javascript/controllers/inline_edit_toggle_controller.js +81 -0
- data/app/javascript/controllers/notifications_controller.js +19 -0
- data/app/javascript/controllers/settings_inline_edit_sync_controller.js +38 -0
- data/app/javascript/controllers/settings_override_controller.js +45 -0
- data/app/mailers/lean_cms/application_mailer.rb +6 -0
- data/app/mailers/lean_cms/passwords_mailer.rb +8 -0
- data/app/mailers/lean_cms/users_mailer.rb +39 -0
- data/app/models/lean_cms/current.rb +6 -0
- data/app/models/lean_cms/form_submission.rb +45 -0
- data/app/models/lean_cms/magic_link.rb +76 -0
- data/app/models/lean_cms/meta_tag.rb +30 -0
- data/app/models/lean_cms/notification_setting.rb +69 -0
- data/app/models/lean_cms/page.rb +23 -0
- data/app/models/lean_cms/page_content.rb +245 -0
- data/app/models/lean_cms/post.rb +65 -0
- data/app/models/lean_cms/session.rb +7 -0
- data/app/models/lean_cms/setting.rb +156 -0
- data/app/policies/lean_cms/application_policy.rb +35 -0
- data/app/policies/lean_cms/page_content_policy.rb +31 -0
- data/app/policies/lean_cms/post_policy.rb +37 -0
- data/app/policies/lean_cms/setting_policy.rb +17 -0
- data/app/views/layouts/lean_cms/application.html.erb +114 -0
- data/app/views/layouts/lean_cms/auth.html.erb +200 -0
- data/app/views/lean_cms/activity/index.html.erb +79 -0
- data/app/views/lean_cms/dashboard/index.html.erb +180 -0
- data/app/views/lean_cms/form_submissions/index.html.erb +104 -0
- data/app/views/lean_cms/form_submissions/show.html.erb +157 -0
- data/app/views/lean_cms/notification_settings/edit.html.erb +192 -0
- data/app/views/lean_cms/notifications/index.html.erb +72 -0
- data/app/views/lean_cms/notifications/show.html.erb +39 -0
- data/app/views/lean_cms/page_contents/_field_editor.html.erb +174 -0
- data/app/views/lean_cms/page_contents/edit.html.erb +428 -0
- data/app/views/lean_cms/page_contents/index.html.erb +113 -0
- data/app/views/lean_cms/password_setup/show.html.erb +35 -0
- data/app/views/lean_cms/passwords/edit.html.erb +26 -0
- data/app/views/lean_cms/passwords/new.html.erb +21 -0
- data/app/views/lean_cms/passwords_mailer/reset.html.erb +6 -0
- data/app/views/lean_cms/passwords_mailer/reset.text.erb +4 -0
- data/app/views/lean_cms/posts/_form.html.erb +118 -0
- data/app/views/lean_cms/posts/edit.html.erb +31 -0
- data/app/views/lean_cms/posts/index.html.erb +100 -0
- data/app/views/lean_cms/posts/new.html.erb +16 -0
- data/app/views/lean_cms/sessions/new.html.erb +28 -0
- data/app/views/lean_cms/settings/edit.html.erb +384 -0
- data/app/views/lean_cms/shared/_admin_bar.html.erb +85 -0
- data/app/views/lean_cms/shared/_header.html.erb +86 -0
- data/app/views/lean_cms/shared/_notifications_bell.html.erb +84 -0
- data/app/views/lean_cms/shared/_sidebar.html.erb +102 -0
- data/app/views/lean_cms/users/_form.html.erb +105 -0
- data/app/views/lean_cms/users/edit.html.erb +8 -0
- data/app/views/lean_cms/users/index.html.erb +99 -0
- data/app/views/lean_cms/users/new.html.erb +8 -0
- data/app/views/lean_cms/users_mailer/admin_triggered_password_reset.html.erb +13 -0
- data/app/views/lean_cms/users_mailer/admin_triggered_password_reset.text.erb +11 -0
- data/app/views/lean_cms/users_mailer/invitation.html.erb +13 -0
- data/app/views/lean_cms/users_mailer/invitation.text.erb +11 -0
- data/app/views/lean_cms/users_mailer/reactivation.html.erb +13 -0
- data/app/views/lean_cms/users_mailer/reactivation.text.erb +11 -0
- data/config/importmap.rb +8 -0
- data/config/routes.rb +78 -0
- data/db/migrate/20251112034030_create_lean_cms_tables.rb +131 -0
- data/db/migrate/20260513000001_create_lean_cms_auth_tables.rb +31 -0
- data/db/migrate/20260514000001_create_paper_trail_versions.rb +16 -0
- data/db/migrate/20260514000002_create_action_text_tables.rb +18 -0
- data/db/migrate/20260514000003_create_active_storage_tables.rb +45 -0
- data/db/migrate/20260514000004_create_noticed_tables.rb +27 -0
- data/lib/generators/lean_cms/demo/demo_generator.rb +54 -0
- data/lib/generators/lean_cms/demo/templates/lean_cms_structure.yml +129 -0
- data/lib/generators/lean_cms/demo/templates/pages_controller.rb +30 -0
- data/lib/generators/lean_cms/demo/templates/views/pages/about.html.erb +40 -0
- data/lib/generators/lean_cms/demo/templates/views/pages/contact.html.erb +55 -0
- data/lib/generators/lean_cms/demo/templates/views/pages/home.html.erb +31 -0
- data/lib/generators/lean_cms/install/install_generator.rb +317 -0
- data/lib/generators/lean_cms/install/templates/add_lean_cms_columns_to_users.rb.tt +7 -0
- data/lib/generators/lean_cms/install/templates/lean_cms.rb +11 -0
- data/lib/generators/lean_cms/install/templates/lean_cms_structure.yml +29 -0
- data/lib/lean_cms/configuration.rb +32 -0
- data/lib/lean_cms/engine.rb +93 -0
- data/lib/lean_cms/loader.rb +217 -0
- data/lib/lean_cms/sync_helper.rb +182 -0
- data/lib/lean_cms/version.rb +3 -0
- data/lib/lean_cms.rb +26 -0
- data/lib/tasks/lean_cms.rake +390 -0
- metadata +313 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module LeanCms
|
|
2
|
+
class PostsController < LeanCms::ApplicationController
|
|
3
|
+
before_action :require_blog_editing
|
|
4
|
+
before_action :set_post, only: [:show, :edit, :update, :destroy]
|
|
5
|
+
before_action :authorize_edit, only: [:edit, :update, :destroy]
|
|
6
|
+
before_action :check_content_lock, only: [:destroy]
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
@posts = LeanCms::Post.includes(:author, :last_edited_by)
|
|
10
|
+
.order(created_at: :desc)
|
|
11
|
+
|
|
12
|
+
# Filter by content_type if provided
|
|
13
|
+
if params[:content_type].present?
|
|
14
|
+
case params[:content_type]
|
|
15
|
+
when 'blog'
|
|
16
|
+
@posts = @posts.blog_posts
|
|
17
|
+
when 'portfolio'
|
|
18
|
+
@posts = @posts.portfolio_items
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def show
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def new
|
|
27
|
+
@post = LeanCms::Post.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def create
|
|
31
|
+
@post = LeanCms::Post.new(post_params)
|
|
32
|
+
@post.author = current_user
|
|
33
|
+
@post.last_edited_by = current_user
|
|
34
|
+
|
|
35
|
+
if @post.save
|
|
36
|
+
redirect_to lean_cms_posts_path, notice: 'Post created successfully.'
|
|
37
|
+
else
|
|
38
|
+
render :new, status: :unprocessable_entity
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def edit
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def update
|
|
46
|
+
@post.last_edited_by = current_user
|
|
47
|
+
|
|
48
|
+
if @post.update(post_params)
|
|
49
|
+
redirect_to lean_cms_posts_path, notice: 'Post updated successfully.'
|
|
50
|
+
else
|
|
51
|
+
render :edit, status: :unprocessable_entity
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def destroy
|
|
56
|
+
@post.destroy
|
|
57
|
+
redirect_to lean_cms_posts_path, notice: 'Post deleted successfully.'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def set_post
|
|
63
|
+
@post = LeanCms::Post.find(params[:id])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def authorize_edit
|
|
67
|
+
unless can_edit?(@post)
|
|
68
|
+
redirect_to lean_cms_posts_path, alert: 'You are not authorized to edit this post.'
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def post_params
|
|
73
|
+
params.require(:lean_cms_post).permit(
|
|
74
|
+
:title, :slug, :excerpt, :body, :status, :published_at, :content_type, :featured_image
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module LeanCms
|
|
2
|
+
class SessionsController < ::ApplicationController
|
|
3
|
+
allow_unauthenticated_access only: %i[ new create ]
|
|
4
|
+
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to lean_cms_new_session_path, alert: "Try again later." }
|
|
5
|
+
|
|
6
|
+
layout "lean_cms/auth"
|
|
7
|
+
|
|
8
|
+
def new
|
|
9
|
+
redirect_to after_authentication_url if authenticated?
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create
|
|
13
|
+
user_class = LeanCms.user_class.constantize
|
|
14
|
+
|
|
15
|
+
if user = user_class.authenticate_by(params.permit(:email_address, :password))
|
|
16
|
+
unless user.active?
|
|
17
|
+
redirect_to lean_cms_new_session_path, alert: "Your account has been deactivated. Please contact an administrator."
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
start_new_session_for user
|
|
22
|
+
user.record_login!
|
|
23
|
+
|
|
24
|
+
if user.must_change_password?
|
|
25
|
+
magic_link = LeanCms::MagicLink.create_for_password_reset(user)
|
|
26
|
+
redirect_to lean_cms_password_setup_path(token: magic_link.token), notice: "Please set a new password."
|
|
27
|
+
else
|
|
28
|
+
redirect_to after_authentication_url
|
|
29
|
+
end
|
|
30
|
+
else
|
|
31
|
+
redirect_to lean_cms_new_session_path, alert: "Try another email address or password."
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def destroy
|
|
36
|
+
terminate_session
|
|
37
|
+
redirect_to lean_cms_new_session_path, status: :see_other
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def after_authentication_url
|
|
43
|
+
if LeanCms::Current.user&.has_any_cms_permission?
|
|
44
|
+
lean_cms_root_path
|
|
45
|
+
else
|
|
46
|
+
session.delete(:return_to_after_authenticating) || "/"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
module LeanCms
|
|
2
|
+
class SettingsController < ApplicationController
|
|
3
|
+
skip_before_action :check_content_lock
|
|
4
|
+
before_action :require_settings_access
|
|
5
|
+
|
|
6
|
+
def edit
|
|
7
|
+
# Load current settings (default to enabled for backward compatibility)
|
|
8
|
+
@in_context_editing_enabled = LeanCms::Setting.get('in_context_editing', 'true') == 'true'
|
|
9
|
+
@show_blog_enabled = LeanCms::Setting.get('show_blog', 'true') == 'true'
|
|
10
|
+
@show_portfolio_enabled = LeanCms::Setting.get('show_portfolio', 'true') == 'true'
|
|
11
|
+
|
|
12
|
+
# Load counts for display
|
|
13
|
+
@blog_count = LeanCms::Post.published.blog_posts.count
|
|
14
|
+
@portfolio_count = LeanCms::Post.published.portfolio_items.count
|
|
15
|
+
|
|
16
|
+
# Blog and Portfolio hero text
|
|
17
|
+
@blog_title = LeanCms::Setting.get('blog_title', 'Our Blog')
|
|
18
|
+
@blog_subtitle = LeanCms::Setting.get('blog_subtitle', 'Insights, updates, and stories from Custom Assembly Services')
|
|
19
|
+
@portfolio_title = LeanCms::Setting.get('portfolio_title', 'Our Portfolio')
|
|
20
|
+
@portfolio_subtitle = LeanCms::Setting.get('portfolio_subtitle', 'Showcasing our industrial assembly and installation projects')
|
|
21
|
+
|
|
22
|
+
# Privacy & Compliance
|
|
23
|
+
@cookie_consent_enabled = LeanCms::Setting.get('cookie_consent_enabled', 'false') == 'true'
|
|
24
|
+
@cookie_consent_message = LeanCms::Setting.get('cookie_consent_message', 'We use cookies to improve your experience and analyze site traffic. You can choose which cookies to allow.')
|
|
25
|
+
@cookie_consent_forced_on = LeanCms::Setting.enabled?('google_analytics_enabled')
|
|
26
|
+
|
|
27
|
+
# Analytics
|
|
28
|
+
@google_analytics_enabled = LeanCms::Setting.enabled?('google_analytics_enabled')
|
|
29
|
+
@google_analytics_id = LeanCms::Setting.get('google_analytics_id', '')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def update
|
|
33
|
+
# Site favicon (uploaded override for the public site)
|
|
34
|
+
LeanCms::Setting.remove_site_favicon! if params[:remove_site_favicon] == "1"
|
|
35
|
+
LeanCms::Setting.update_site_favicon!(params[:site_favicon]) if params[:site_favicon].present?
|
|
36
|
+
|
|
37
|
+
# Update in-context editing setting
|
|
38
|
+
LeanCms::Setting.set('in_context_editing', params[:in_context_editing] == '1' ? 'true' : 'false')
|
|
39
|
+
|
|
40
|
+
# Update show blog and portfolio settings
|
|
41
|
+
LeanCms::Setting.set('show_blog', params[:show_blog] == '1' ? 'true' : 'false')
|
|
42
|
+
LeanCms::Setting.set('show_portfolio', params[:show_portfolio] == '1' ? 'true' : 'false')
|
|
43
|
+
|
|
44
|
+
# Update blog and portfolio hero text
|
|
45
|
+
LeanCms::Setting.set('blog_title', params[:blog_title].to_s) if params.key?(:blog_title)
|
|
46
|
+
LeanCms::Setting.set('blog_subtitle', params[:blog_subtitle].to_s) if params.key?(:blog_subtitle)
|
|
47
|
+
LeanCms::Setting.set('portfolio_title', params[:portfolio_title].to_s) if params.key?(:portfolio_title)
|
|
48
|
+
LeanCms::Setting.set('portfolio_subtitle', params[:portfolio_subtitle].to_s) if params.key?(:portfolio_subtitle)
|
|
49
|
+
|
|
50
|
+
# Update site address (as structured JSON)
|
|
51
|
+
address_data = {
|
|
52
|
+
'street1' => params[:site_street1].to_s,
|
|
53
|
+
'street2' => params[:site_street2].to_s,
|
|
54
|
+
'city' => params[:site_city].to_s,
|
|
55
|
+
'state' => params[:site_state].to_s.upcase,
|
|
56
|
+
'zip' => params[:site_zip].to_s
|
|
57
|
+
}
|
|
58
|
+
LeanCms::Setting.set_json('site_address', address_data)
|
|
59
|
+
|
|
60
|
+
# Update phone and email
|
|
61
|
+
LeanCms::Setting.set('site_phone', params[:site_phone]) if params[:site_phone]
|
|
62
|
+
LeanCms::Setting.set('site_email', params[:site_email]) if params[:site_email]
|
|
63
|
+
|
|
64
|
+
# Update business hours (as JSON)
|
|
65
|
+
if params[:business_hours_labels] || params[:business_hours_note]
|
|
66
|
+
hours_data = {
|
|
67
|
+
'hours' => build_hours_array,
|
|
68
|
+
'note' => params[:business_hours_note].to_s
|
|
69
|
+
}
|
|
70
|
+
LeanCms::Setting.set_json('business_hours', hours_data)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Update Google Analytics (when enabled, force cookie consent on)
|
|
74
|
+
LeanCms::Setting.set('google_analytics_enabled', params[:google_analytics_enabled] == '1' ? 'true' : 'false')
|
|
75
|
+
LeanCms::Setting.set('google_analytics_id', params[:google_analytics_id].to_s.strip) if params.key?(:google_analytics_id)
|
|
76
|
+
LeanCms::Setting.set('cookie_consent_enabled', 'true') if LeanCms::Setting.enabled?('google_analytics_enabled')
|
|
77
|
+
|
|
78
|
+
# Update cookie consent (cannot disable if GA is enabled)
|
|
79
|
+
if params.key?(:cookie_consent_enabled)
|
|
80
|
+
forced_on = LeanCms::Setting.enabled?('google_analytics_enabled')
|
|
81
|
+
value = params[:cookie_consent_enabled] == '1' ? 'true' : 'false'
|
|
82
|
+
LeanCms::Setting.set('cookie_consent_enabled', forced_on ? 'true' : value)
|
|
83
|
+
end
|
|
84
|
+
LeanCms::Setting.set('cookie_consent_message', params[:cookie_consent_message].to_s) if params.key?(:cookie_consent_message)
|
|
85
|
+
|
|
86
|
+
redirect_to lean_cms_settings_path, notice: 'Settings updated successfully.'
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def lock
|
|
90
|
+
reason = params[:reason].presence || 'Content sync in progress'
|
|
91
|
+
LeanCms::Setting.lock_content!(reason)
|
|
92
|
+
redirect_to lean_cms_settings_path, notice: "Content editing locked: #{reason}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def unlock
|
|
96
|
+
LeanCms::Setting.unlock_content!
|
|
97
|
+
redirect_to lean_cms_settings_path, notice: 'Content editing unlocked. Editors can now make changes.'
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# AJAX endpoint to toggle override settings
|
|
101
|
+
def update_override
|
|
102
|
+
allowed_keys = %w[contact_info_override contact_hours_override]
|
|
103
|
+
key = params[:key]
|
|
104
|
+
value = params[:value]
|
|
105
|
+
|
|
106
|
+
if allowed_keys.include?(key) && %w[true false].include?(value)
|
|
107
|
+
LeanCms::Setting.set(key, value)
|
|
108
|
+
head :ok
|
|
109
|
+
else
|
|
110
|
+
head :unprocessable_entity
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def build_hours_array
|
|
117
|
+
return [] unless params[:business_hours_labels]
|
|
118
|
+
params[:business_hours_labels]
|
|
119
|
+
.zip(params[:business_hours_values] || [])
|
|
120
|
+
.map { |label, value| { 'label' => label.to_s, 'value' => value.to_s } }
|
|
121
|
+
.reject { |h| h['label'].blank? && h['value'].blank? }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module LeanCms
|
|
2
|
+
class UsersController < ApplicationController
|
|
3
|
+
before_action :set_user, only: [:show, :edit, :update, :deactivate, :activate, :send_password_reset]
|
|
4
|
+
after_action :verify_authorized
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
authorize User
|
|
8
|
+
@users = policy_scope(User).includes(:sessions).order(created_at: :desc)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def show
|
|
12
|
+
authorize @user
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def new
|
|
16
|
+
@user = User.new
|
|
17
|
+
authorize @user
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create
|
|
21
|
+
@user = User.new(user_params)
|
|
22
|
+
@user.active = false # Will be activated when they set their password
|
|
23
|
+
@user.password = SecureRandom.hex(32) # Temporary password, will be replaced
|
|
24
|
+
authorize @user
|
|
25
|
+
|
|
26
|
+
if @user.save
|
|
27
|
+
magic_link = MagicLink.create_for_invitation(@user, created_by_ip: request.remote_ip)
|
|
28
|
+
UsersMailer.invitation(@user, magic_link).deliver_later
|
|
29
|
+
redirect_to lean_cms_users_path, notice: "User invited. They will receive an email to set their password."
|
|
30
|
+
else
|
|
31
|
+
render :new, status: :unprocessable_entity
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def edit
|
|
36
|
+
authorize @user
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def update
|
|
40
|
+
authorize @user
|
|
41
|
+
|
|
42
|
+
# Prevent non-super-admins from granting super admin or settings access
|
|
43
|
+
if !current_user.is_super_admin?
|
|
44
|
+
if params[:user][:is_super_admin] == "1" || params[:user][:is_super_admin] == true
|
|
45
|
+
flash[:alert] = "Only super admins can grant super admin privileges."
|
|
46
|
+
render :edit, status: :unprocessable_entity
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
if params[:user][:can_access_settings] == "1" || params[:user][:can_access_settings] == true
|
|
50
|
+
flash[:alert] = "Only super admins can grant settings access."
|
|
51
|
+
render :edit, status: :unprocessable_entity
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if @user.update(user_params)
|
|
57
|
+
redirect_to lean_cms_users_path, notice: "User updated successfully."
|
|
58
|
+
else
|
|
59
|
+
render :edit, status: :unprocessable_entity
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def deactivate
|
|
64
|
+
authorize @user
|
|
65
|
+
|
|
66
|
+
if @user == current_user
|
|
67
|
+
redirect_to lean_cms_users_path, alert: "You cannot deactivate your own account."
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
@user.deactivate!
|
|
72
|
+
redirect_to lean_cms_users_path, notice: "User deactivated."
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def activate
|
|
76
|
+
authorize @user
|
|
77
|
+
|
|
78
|
+
# Send a password reset link when activating a previously deactivated user
|
|
79
|
+
magic_link = MagicLink.create_for_password_reset(@user, created_by_ip: request.remote_ip)
|
|
80
|
+
UsersMailer.reactivation(@user, magic_link).deliver_later
|
|
81
|
+
@user.activate!
|
|
82
|
+
|
|
83
|
+
redirect_to lean_cms_users_path, notice: "User activated. They will receive an email to set a new password."
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def send_password_reset
|
|
87
|
+
authorize @user
|
|
88
|
+
|
|
89
|
+
magic_link = MagicLink.create_for_password_reset(@user, created_by_ip: request.remote_ip)
|
|
90
|
+
UsersMailer.admin_triggered_password_reset(@user, magic_link).deliver_later
|
|
91
|
+
|
|
92
|
+
redirect_to lean_cms_users_path, notice: "Password reset email sent to #{@user.email_address}."
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def set_user
|
|
98
|
+
@user = User.find(params[:id])
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def user_params
|
|
102
|
+
permitted = [:name, :email_address, :can_edit_pages, :can_edit_blog, :can_manage_users]
|
|
103
|
+
|
|
104
|
+
# Only super admins can modify these permissions
|
|
105
|
+
if current_user.is_super_admin?
|
|
106
|
+
permitted << :can_access_settings
|
|
107
|
+
permitted << :is_super_admin
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
params.require(:user).permit(permitted)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
module LeanCms
|
|
2
|
+
module ActivityHelper
|
|
3
|
+
def activity_item_label(version)
|
|
4
|
+
case version.item_type
|
|
5
|
+
when "LeanCms::Setting"
|
|
6
|
+
setting = parsed_setting_object(version)
|
|
7
|
+
"Setting: #{setting&.dig('key') || version.item_id}"
|
|
8
|
+
when "LeanCms::PageContent"
|
|
9
|
+
pc = version.item_type.constantize.unscoped.find_by(id: version.item_id)
|
|
10
|
+
if pc
|
|
11
|
+
page_slug = pc.page&.full_path || pc.page&.slug || pc.read_attribute(:page).to_s
|
|
12
|
+
"Page Content: #{page_slug}/#{pc.section}/#{pc.key}"
|
|
13
|
+
else
|
|
14
|
+
"Page Content ##{version.item_id}"
|
|
15
|
+
end
|
|
16
|
+
when "LeanCms::Post"
|
|
17
|
+
post = version.item_type.constantize.unscoped.find_by(id: version.item_id)
|
|
18
|
+
post ? "Post: #{post.title}" : "Post ##{version.item_id}"
|
|
19
|
+
when "User"
|
|
20
|
+
user = User.unscoped.find_by(id: version.item_id)
|
|
21
|
+
user ? "User: #{user.email_address}" : "User ##{version.item_id}"
|
|
22
|
+
when "LeanCms::FormSubmission"
|
|
23
|
+
fs = version.item_type.constantize.unscoped.find_by(id: version.item_id)
|
|
24
|
+
fs ? "Form Submission ##{version.item_id}" : "Form Submission ##{version.item_id}"
|
|
25
|
+
else
|
|
26
|
+
"#{version.item_type} ##{version.item_id}"
|
|
27
|
+
end
|
|
28
|
+
rescue
|
|
29
|
+
"#{version.item_type} ##{version.item_id}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def activity_who(version)
|
|
33
|
+
return "System" if version.whodunnit.blank?
|
|
34
|
+
user = User.unscoped.find_by(id: version.whodunnit)
|
|
35
|
+
user ? user.email_address : "User ##{version.whodunnit}"
|
|
36
|
+
rescue
|
|
37
|
+
"User ##{version.whodunnit}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def activity_action_badge_class(event)
|
|
41
|
+
case event.to_s
|
|
42
|
+
when "create" then "bg-green-100 text-green-800"
|
|
43
|
+
when "update" then "bg-blue-100 text-blue-800"
|
|
44
|
+
when "destroy" then "bg-red-100 text-red-800"
|
|
45
|
+
else "bg-gray-100 text-gray-800"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def activity_old_value(version)
|
|
50
|
+
return "—" if version.event == "create"
|
|
51
|
+
obj = parsed_version_object(version)
|
|
52
|
+
return "—" if obj.blank?
|
|
53
|
+
|
|
54
|
+
format_attributes_for_display(obj, version.item_type)
|
|
55
|
+
rescue
|
|
56
|
+
"—"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def activity_new_value(version)
|
|
60
|
+
return "—" if version.event == "destroy"
|
|
61
|
+
obj = parsed_version_object(version)
|
|
62
|
+
return "—" if obj.blank? && version.event != "create"
|
|
63
|
+
|
|
64
|
+
case version.item_type
|
|
65
|
+
when "LeanCms::Setting"
|
|
66
|
+
if version.event == "create"
|
|
67
|
+
obj["value"].to_s.truncate(80)
|
|
68
|
+
else
|
|
69
|
+
item = version.item_type.constantize.unscoped.find_by(id: version.item_id)
|
|
70
|
+
item&.value&.to_s&.truncate(80) || "—"
|
|
71
|
+
end
|
|
72
|
+
else
|
|
73
|
+
if version.event == "create"
|
|
74
|
+
format_attributes_for_display(obj, version.item_type)
|
|
75
|
+
else
|
|
76
|
+
item = version.item_type.constantize.unscoped.find_by(id: version.item_id)
|
|
77
|
+
item ? truncate_item_summary(item) : "—"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
rescue
|
|
81
|
+
"—"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def parsed_version_object(version)
|
|
87
|
+
return {} if version.object.blank?
|
|
88
|
+
YAML.safe_load(
|
|
89
|
+
version.object,
|
|
90
|
+
permitted_classes: [Symbol, Time, ActiveSupport::TimeWithZone, ActiveSupport::TimeZone, Date, DateTime],
|
|
91
|
+
aliases: true
|
|
92
|
+
) || {}
|
|
93
|
+
rescue
|
|
94
|
+
{}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def parsed_setting_object(version)
|
|
98
|
+
parsed_version_object(version)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def truncate_item_summary(item)
|
|
102
|
+
case item.class.name
|
|
103
|
+
when "LeanCms::PageContent"
|
|
104
|
+
page_slug = item.page&.full_path || item.page&.slug || item.read_attribute(:page).to_s
|
|
105
|
+
value_summary = format_display_value(item.display_value)
|
|
106
|
+
"#{page_slug}/#{item.section}/#{item.key}: #{value_summary}"
|
|
107
|
+
when "LeanCms::Post"
|
|
108
|
+
item.title.to_s.truncate(60)
|
|
109
|
+
when "LeanCms::Page"
|
|
110
|
+
"#{item.title} (#{item.slug})"
|
|
111
|
+
when "User"
|
|
112
|
+
item.email_address.to_s
|
|
113
|
+
when "LeanCms::FormSubmission"
|
|
114
|
+
"Submission ##{item.id}"
|
|
115
|
+
else
|
|
116
|
+
item.to_s.truncate(60)
|
|
117
|
+
end
|
|
118
|
+
rescue
|
|
119
|
+
"—"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def format_attributes_for_display(obj, item_type)
|
|
123
|
+
case item_type
|
|
124
|
+
when "LeanCms::Setting"
|
|
125
|
+
obj["value"].to_s.truncate(80)
|
|
126
|
+
when "LeanCms::PageContent"
|
|
127
|
+
page_slug = safe_page_slug(obj["page"])
|
|
128
|
+
section = obj["section"].to_s.presence || "—"
|
|
129
|
+
key = obj["key"].to_s.presence || "—"
|
|
130
|
+
value_summary = format_value_from_attributes(obj)
|
|
131
|
+
"#{page_slug}/#{section}/#{key}: #{value_summary}"
|
|
132
|
+
when "LeanCms::Post"
|
|
133
|
+
parts = []
|
|
134
|
+
parts << obj["title"].to_s if obj["title"].present?
|
|
135
|
+
parts << obj["excerpt"].to_s.truncate(40) if obj["excerpt"].present?
|
|
136
|
+
parts.any? ? parts.join(" — ").truncate(80) : "Post ##{obj["id"]}"
|
|
137
|
+
when "LeanCms::Page"
|
|
138
|
+
title = obj["title"].to_s.presence || "—"
|
|
139
|
+
slug = obj["slug"].to_s.presence || "—"
|
|
140
|
+
"#{title} (#{slug})"
|
|
141
|
+
when "User"
|
|
142
|
+
obj["email_address"].to_s.presence || obj["name"].to_s.presence || "User ##{obj["id"]}"
|
|
143
|
+
when "LeanCms::FormSubmission"
|
|
144
|
+
"Submission ##{obj["id"]}"
|
|
145
|
+
else
|
|
146
|
+
safe_attributes_summary(obj)
|
|
147
|
+
end
|
|
148
|
+
rescue
|
|
149
|
+
"—"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def safe_page_slug(page_attr)
|
|
153
|
+
return "—" if page_attr.blank?
|
|
154
|
+
return page_attr.to_s if page_attr.is_a?(String)
|
|
155
|
+
page_attr.respond_to?(:slug) ? page_attr.slug.to_s : page_attr.respond_to?(:full_path) ? page_attr.full_path.to_s : "—"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def format_value_from_attributes(obj)
|
|
159
|
+
val = obj["value"].to_s.presence || obj["content"].to_s.presence
|
|
160
|
+
return "—" if val.blank?
|
|
161
|
+
stripped = val.gsub(/<[^>]*>/, " ").squish
|
|
162
|
+
stripped.present? ? stripped.truncate(50) : "—"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def format_display_value(val)
|
|
166
|
+
return "—" if val.blank?
|
|
167
|
+
return val.truncate(40) if val.is_a?(String)
|
|
168
|
+
return "Cards (#{val.size})" if val.is_a?(Array)
|
|
169
|
+
return "Image attached" if val.respond_to?(:attached?) && val.attached?
|
|
170
|
+
return "Image attached" if val.is_a?(ActiveStorage::Attached::One)
|
|
171
|
+
val.to_s.truncate(40)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def safe_attributes_summary(obj)
|
|
175
|
+
skip = %w[created_at updated_at id]
|
|
176
|
+
filtered = obj.except(*skip).transform_values { |v| safe_attribute_value(v) }
|
|
177
|
+
filtered.reject! { |_, v| v.blank? }
|
|
178
|
+
filtered.any? ? filtered.map { |k, v| "#{k}: #{v}" }.join("; ").truncate(80) : "—"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def safe_attribute_value(v)
|
|
182
|
+
return v.to_s if v.is_a?(String) || v.is_a?(Numeric) || v == true || v == false
|
|
183
|
+
return v.strftime("%Y-%m-%d %H:%M") if v.respond_to?(:strftime)
|
|
184
|
+
return v.slug.to_s if v.respond_to?(:slug)
|
|
185
|
+
return v.title.to_s if v.respond_to?(:title)
|
|
186
|
+
return v.email_address.to_s if v.respond_to?(:email_address)
|
|
187
|
+
"—"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module LeanCms
|
|
2
|
+
module ApplicationHelper
|
|
3
|
+
# Darken a hex color by a percentage
|
|
4
|
+
def darken_color(hex_color, percent)
|
|
5
|
+
hex_color = hex_color.gsub('#', '')
|
|
6
|
+
rgb = hex_color.scan(/../).map { |color| color.to_i(16) }
|
|
7
|
+
rgb = rgb.map { |channel| [(channel * (100 - percent) / 100).round, 0].max }
|
|
8
|
+
"##{rgb.map { |channel| channel.to_s(16).rjust(2, '0') }.join}"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Format date for display
|
|
12
|
+
def format_date(date)
|
|
13
|
+
return unless date
|
|
14
|
+
date.strftime("%B %d, %Y")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Format datetime for display
|
|
18
|
+
def format_datetime(datetime)
|
|
19
|
+
return unless datetime
|
|
20
|
+
datetime.strftime("%B %d, %Y at %I:%M %p")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Status badge colors
|
|
24
|
+
def status_badge_class(status)
|
|
25
|
+
case status.to_s
|
|
26
|
+
when 'published'
|
|
27
|
+
'bg-green-100 text-green-800'
|
|
28
|
+
when 'draft'
|
|
29
|
+
'bg-yellow-100 text-yellow-800'
|
|
30
|
+
when 'new_submission'
|
|
31
|
+
'bg-blue-100 text-blue-800'
|
|
32
|
+
when 'read'
|
|
33
|
+
'bg-gray-100 text-gray-800'
|
|
34
|
+
when 'replied'
|
|
35
|
+
'bg-green-100 text-green-800'
|
|
36
|
+
when 'archived'
|
|
37
|
+
'bg-gray-100 text-gray-600'
|
|
38
|
+
else
|
|
39
|
+
'bg-gray-100 text-gray-800'
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module LeanCms
|
|
2
|
+
module ContentHelper
|
|
3
|
+
# Render editable content section
|
|
4
|
+
def render_editable_section(page_key, section_key, default: nil, **options)
|
|
5
|
+
content = LeanCms::PageContent.for_section(page_key, section_key)
|
|
6
|
+
|
|
7
|
+
if content.persisted?
|
|
8
|
+
case content.content_type.to_sym
|
|
9
|
+
when :rich
|
|
10
|
+
content_tag(:div, content.rich_content, **options)
|
|
11
|
+
when :markdown
|
|
12
|
+
# TODO: Add markdown rendering with a gem like Redcarpet
|
|
13
|
+
content_tag(:div, simple_format(content.content), **options)
|
|
14
|
+
else
|
|
15
|
+
content_tag(:div, content.content, **options)
|
|
16
|
+
end
|
|
17
|
+
else
|
|
18
|
+
default
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Check if current user can edit CMS content
|
|
23
|
+
def can_edit_cms?
|
|
24
|
+
current_user&.has_any_cms_permission?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Show edit link if user is CMS editor
|
|
28
|
+
def cms_edit_link(path, text: "Edit", css_class: "")
|
|
29
|
+
return unless can_edit_cms?
|
|
30
|
+
|
|
31
|
+
link_to text, path, class: "cms-edit-link #{css_class}".strip
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|