rsb-admin 0.9.1
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/LICENSE +15 -0
- data/README.md +83 -0
- data/Rakefile +25 -0
- data/app/assets/javascripts/rsb/admin/themes/modern.js +37 -0
- data/app/assets/stylesheets/rsb/admin/themes/default.css +1358 -0
- data/app/assets/stylesheets/rsb/admin/themes/modern.css +1370 -0
- data/app/controllers/concerns/rsb/admin/authorization.rb +21 -0
- data/app/controllers/rsb/admin/admin_controller.rb +138 -0
- data/app/controllers/rsb/admin/admin_users_controller.rb +110 -0
- data/app/controllers/rsb/admin/dashboard_controller.rb +76 -0
- data/app/controllers/rsb/admin/profile_controller.rb +146 -0
- data/app/controllers/rsb/admin/profile_sessions_controller.rb +45 -0
- data/app/controllers/rsb/admin/resources_controller.rb +386 -0
- data/app/controllers/rsb/admin/roles_controller.rb +99 -0
- data/app/controllers/rsb/admin/sessions_controller.rb +139 -0
- data/app/controllers/rsb/admin/settings_controller.rb +203 -0
- data/app/controllers/rsb/admin/two_factor_controller.rb +105 -0
- data/app/helpers/rsb/admin/authorization_helper.rb +49 -0
- data/app/helpers/rsb/admin/branding_helper.rb +38 -0
- data/app/helpers/rsb/admin/formatting_helper.rb +205 -0
- data/app/helpers/rsb/admin/i18n_helper.rb +148 -0
- data/app/helpers/rsb/admin/icons_helper.rb +55 -0
- data/app/helpers/rsb/admin/table_helper.rb +132 -0
- data/app/helpers/rsb/admin/theme_helper.rb +84 -0
- data/app/helpers/rsb/admin/url_helper.rb +109 -0
- data/app/mailers/rsb/admin/admin_mailer.rb +37 -0
- data/app/models/rsb/admin/admin_session.rb +109 -0
- data/app/models/rsb/admin/admin_user.rb +153 -0
- data/app/models/rsb/admin/application_record.rb +10 -0
- data/app/models/rsb/admin/role.rb +63 -0
- data/app/views/layouts/rsb/admin/application.html.erb +45 -0
- data/app/views/rsb/admin/admin_mailer/email_verification.html.erb +11 -0
- data/app/views/rsb/admin/admin_mailer/email_verification.text.erb +11 -0
- data/app/views/rsb/admin/admin_users/_form.html.erb +52 -0
- data/app/views/rsb/admin/admin_users/edit.html.erb +10 -0
- data/app/views/rsb/admin/admin_users/index.html.erb +77 -0
- data/app/views/rsb/admin/admin_users/new.html.erb +10 -0
- data/app/views/rsb/admin/admin_users/show.html.erb +85 -0
- data/app/views/rsb/admin/dashboard/index.html.erb +36 -0
- data/app/views/rsb/admin/profile/edit.html.erb +67 -0
- data/app/views/rsb/admin/profile/show.html.erb +155 -0
- data/app/views/rsb/admin/resources/_filters.html.erb +58 -0
- data/app/views/rsb/admin/resources/_form.html.erb +20 -0
- data/app/views/rsb/admin/resources/_pagination.html.erb +33 -0
- data/app/views/rsb/admin/resources/_table.html.erb +70 -0
- data/app/views/rsb/admin/resources/edit.html.erb +7 -0
- data/app/views/rsb/admin/resources/index.html.erb +49 -0
- data/app/views/rsb/admin/resources/new.html.erb +7 -0
- data/app/views/rsb/admin/resources/page.html.erb +9 -0
- data/app/views/rsb/admin/resources/show.html.erb +55 -0
- data/app/views/rsb/admin/roles/_form.html.erb +197 -0
- data/app/views/rsb/admin/roles/edit.html.erb +7 -0
- data/app/views/rsb/admin/roles/index.html.erb +71 -0
- data/app/views/rsb/admin/roles/new.html.erb +7 -0
- data/app/views/rsb/admin/roles/show.html.erb +99 -0
- data/app/views/rsb/admin/sessions/new.html.erb +31 -0
- data/app/views/rsb/admin/sessions/two_factor.html.erb +39 -0
- data/app/views/rsb/admin/settings/_field.html.erb +115 -0
- data/app/views/rsb/admin/settings/index.html.erb +61 -0
- data/app/views/rsb/admin/shared/_badge.html.erb +1 -0
- data/app/views/rsb/admin/shared/_breadcrumbs.html.erb +12 -0
- data/app/views/rsb/admin/shared/_empty_state.html.erb +4 -0
- data/app/views/rsb/admin/shared/_flash.html.erb +22 -0
- data/app/views/rsb/admin/shared/_header.html.erb +50 -0
- data/app/views/rsb/admin/shared/_page_tabs.html.erb +21 -0
- data/app/views/rsb/admin/shared/_sidebar.html.erb +99 -0
- data/app/views/rsb/admin/shared/disabled.html.erb +38 -0
- data/app/views/rsb/admin/shared/fields/_checkbox.html.erb +6 -0
- data/app/views/rsb/admin/shared/fields/_datetime.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_email.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_hidden.html.erb +1 -0
- data/app/views/rsb/admin/shared/fields/_json.html.erb +11 -0
- data/app/views/rsb/admin/shared/fields/_number.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_password.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_select.html.erb +12 -0
- data/app/views/rsb/admin/shared/fields/_text.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_textarea.html.erb +10 -0
- data/app/views/rsb/admin/shared/forbidden.html.erb +22 -0
- data/app/views/rsb/admin/themes/modern/views/shared/_header.html.erb +77 -0
- data/app/views/rsb/admin/themes/modern/views/shared/_sidebar.html.erb +135 -0
- data/app/views/rsb/admin/two_factor/backup_codes.html.erb +48 -0
- data/app/views/rsb/admin/two_factor/new.html.erb +53 -0
- data/config/locales/en.yml +140 -0
- data/config/locales/seo.en.yml +21 -0
- data/config/routes.rb +59 -0
- data/db/migrate/20260208000003_create_rsb_admin_tables.rb +43 -0
- data/db/migrate/20260214000001_add_otp_fields_to_rsb_admin_admin_users.rb +9 -0
- data/lib/generators/rsb/admin/install/install_generator.rb +45 -0
- data/lib/generators/rsb/admin/install/templates/rsb_admin_seeds.rb +24 -0
- data/lib/generators/rsb/admin/theme/templates/theme.css.tt +66 -0
- data/lib/generators/rsb/admin/theme/theme_generator.rb +218 -0
- data/lib/generators/rsb/admin/views/views_generator.rb +262 -0
- data/lib/rsb/admin/breadcrumb_item.rb +26 -0
- data/lib/rsb/admin/category_registration.rb +177 -0
- data/lib/rsb/admin/column_definition.rb +89 -0
- data/lib/rsb/admin/configuration.rb +69 -0
- data/lib/rsb/admin/engine.rb +34 -0
- data/lib/rsb/admin/filter_definition.rb +129 -0
- data/lib/rsb/admin/form_field_definition.rb +96 -0
- data/lib/rsb/admin/icons.rb +95 -0
- data/lib/rsb/admin/page_registration.rb +140 -0
- data/lib/rsb/admin/registry.rb +109 -0
- data/lib/rsb/admin/resource_dsl_context.rb +139 -0
- data/lib/rsb/admin/resource_registration.rb +287 -0
- data/lib/rsb/admin/settings_schema.rb +60 -0
- data/lib/rsb/admin/test_kit/helpers.rb +316 -0
- data/lib/rsb/admin/test_kit/resource_test_case.rb +193 -0
- data/lib/rsb/admin/test_kit.rb +11 -0
- data/lib/rsb/admin/theme_definition.rb +46 -0
- data/lib/rsb/admin/themes/modern.rb +44 -0
- data/lib/rsb/admin/version.rb +9 -0
- data/lib/rsb/admin.rb +177 -0
- data/lib/tasks/rsb/admin_tasks.rake +23 -0
- metadata +227 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSB
|
|
4
|
+
module Admin
|
|
5
|
+
module Authorization
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def authorize_admin_action!(resource: nil, action: nil)
|
|
11
|
+
resource ||= controller_name
|
|
12
|
+
action ||= action_name
|
|
13
|
+
|
|
14
|
+
return if current_admin_user.can?(resource, action)
|
|
15
|
+
|
|
16
|
+
render template: 'rsb/admin/shared/forbidden', status: :forbidden
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSB
|
|
4
|
+
module Admin
|
|
5
|
+
class AdminController < ActionController::Base
|
|
6
|
+
layout -> { RSB::Admin.configuration.layout }
|
|
7
|
+
|
|
8
|
+
include Authorization
|
|
9
|
+
|
|
10
|
+
helper RSB::Admin::IconsHelper
|
|
11
|
+
helper RSB::Admin::ThemeHelper
|
|
12
|
+
helper RSB::Admin::FormattingHelper
|
|
13
|
+
helper RSB::Admin::I18nHelper
|
|
14
|
+
helper RSB::Admin::TableHelper
|
|
15
|
+
helper RSB::Admin::AuthorizationHelper
|
|
16
|
+
helper RSB::Admin::BrandingHelper
|
|
17
|
+
helper RSB::Admin::UrlHelper
|
|
18
|
+
include RSB::Admin::UrlHelper
|
|
19
|
+
helper RSB::Settings::LocaleHelper
|
|
20
|
+
include RSB::Settings::LocaleHelper
|
|
21
|
+
helper RSB::Settings::SeoHelper
|
|
22
|
+
include RSB::Settings::SeoHelper
|
|
23
|
+
|
|
24
|
+
before_action :set_seo_context
|
|
25
|
+
before_action :check_admin_enabled
|
|
26
|
+
before_action :require_admin_authentication
|
|
27
|
+
before_action :enforce_two_factor_enrollment
|
|
28
|
+
before_action :build_breadcrumbs
|
|
29
|
+
before_action :track_session_activity
|
|
30
|
+
|
|
31
|
+
helper_method :current_admin_user, :admin_registry, :breadcrumbs, :current_admin_session
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def set_seo_context
|
|
36
|
+
@rsb_seo_context = :admin
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def check_admin_enabled
|
|
40
|
+
return if RSB::Admin.enabled?
|
|
41
|
+
|
|
42
|
+
render template: 'rsb/admin/shared/disabled', layout: false, status: :service_unavailable
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def require_admin_authentication
|
|
46
|
+
return if current_admin_user
|
|
47
|
+
|
|
48
|
+
redirect_to rsb_admin.login_path, alert: 'Please sign in.'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def enforce_two_factor_enrollment
|
|
52
|
+
return unless current_admin_user
|
|
53
|
+
return unless ActiveModel::Type::Boolean.new.cast(RSB::Settings.get('admin.require_two_factor'))
|
|
54
|
+
return if current_admin_user.otp_enabled?
|
|
55
|
+
|
|
56
|
+
# Allow access to TwoFactorController and logout
|
|
57
|
+
return if is_a?(RSB::Admin::TwoFactorController)
|
|
58
|
+
return if controller_name == 'sessions' && action_name == 'destroy'
|
|
59
|
+
|
|
60
|
+
redirect_to rsb_admin.new_profile_two_factor_path,
|
|
61
|
+
alert: 'Two-factor authentication is required. Please set up 2FA to continue.'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def current_admin_user
|
|
65
|
+
return @current_admin_user if defined?(@current_admin_user)
|
|
66
|
+
|
|
67
|
+
token = session[:rsb_admin_session_token]
|
|
68
|
+
@current_admin_session = token ? AdminSession.find_by(session_token: token) : nil
|
|
69
|
+
@current_admin_user = @current_admin_session&.admin_user
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Returns the current admin session record for this request.
|
|
73
|
+
# Must call current_admin_user first to populate.
|
|
74
|
+
#
|
|
75
|
+
# @return [AdminSession, nil]
|
|
76
|
+
def current_admin_session
|
|
77
|
+
current_admin_user unless defined?(@current_admin_session)
|
|
78
|
+
@current_admin_session
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Touches the current session's last_active_at timestamp.
|
|
82
|
+
# Runs on every authenticated request. Uses update_column
|
|
83
|
+
# to avoid callbacks/timestamps overhead.
|
|
84
|
+
#
|
|
85
|
+
# @return [void]
|
|
86
|
+
def track_session_activity
|
|
87
|
+
current_admin_session&.touch_activity!
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def admin_registry
|
|
91
|
+
RSB::Admin.registry
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def breadcrumbs
|
|
95
|
+
@breadcrumbs || []
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Builds the initial breadcrumb trail with the app name as root.
|
|
99
|
+
# The root item links to the dashboard path as the home destination.
|
|
100
|
+
# Subclasses call super and then add their own items via add_breadcrumb.
|
|
101
|
+
#
|
|
102
|
+
# If breadcrumbs were passed via request.env (from Rack dispatch),
|
|
103
|
+
# those are used instead of building from scratch.
|
|
104
|
+
#
|
|
105
|
+
# @return [void]
|
|
106
|
+
def build_breadcrumbs
|
|
107
|
+
if request.env['rsb.admin.breadcrumbs']
|
|
108
|
+
@breadcrumbs = request.env['rsb.admin.breadcrumbs'].dup
|
|
109
|
+
return
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
@breadcrumbs = [
|
|
113
|
+
RSB::Admin::BreadcrumbItem.new(
|
|
114
|
+
label: RSB::Settings.get('admin.app_name').to_s.presence || RSB::Admin.configuration.app_name,
|
|
115
|
+
path: rsb_admin.dashboard_path
|
|
116
|
+
)
|
|
117
|
+
]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Appends a breadcrumb item to the current trail.
|
|
121
|
+
#
|
|
122
|
+
# @param label [String] the text to display
|
|
123
|
+
# @param path [String, nil] the URL path (nil for current/last item)
|
|
124
|
+
# @return [void]
|
|
125
|
+
def add_breadcrumb(label, path = nil)
|
|
126
|
+
@breadcrumbs << RSB::Admin::BreadcrumbItem.new(label: label, path: path)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Replaces the entire breadcrumb trail.
|
|
130
|
+
#
|
|
131
|
+
# @param items [Array<RSB::Admin::BreadcrumbItem>] the new breadcrumb items
|
|
132
|
+
# @return [void]
|
|
133
|
+
def set_breadcrumbs(items)
|
|
134
|
+
@breadcrumbs = items
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSB
|
|
4
|
+
module Admin
|
|
5
|
+
# Handles CRUD operations for admin user accounts.
|
|
6
|
+
# Provides listing, viewing, creating, editing, updating,
|
|
7
|
+
# and deleting admin users, with self-deletion protection.
|
|
8
|
+
class AdminUsersController < AdminController
|
|
9
|
+
before_action :authorize_admin_users
|
|
10
|
+
before_action :set_admin_user, only: %i[show edit update destroy]
|
|
11
|
+
|
|
12
|
+
# GET /admin/admin_users
|
|
13
|
+
# Lists all admin users ordered by email, eagerly loading roles.
|
|
14
|
+
def index
|
|
15
|
+
@rsb_page_title = I18n.t('rsb.admin.admin_users.index.page_title', default: 'Admin Users')
|
|
16
|
+
@admin_users = AdminUser.includes(:role).order(:email)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# GET /admin/admin_users/:id
|
|
20
|
+
# Displays details for a single admin user.
|
|
21
|
+
def show; end
|
|
22
|
+
|
|
23
|
+
# GET /admin/admin_users/new
|
|
24
|
+
# Renders the form for creating a new admin user.
|
|
25
|
+
def new
|
|
26
|
+
@admin_user = AdminUser.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# POST /admin/admin_users
|
|
30
|
+
# Creates a new admin user with the given params.
|
|
31
|
+
# Redirects to show on success, re-renders form on failure.
|
|
32
|
+
def create
|
|
33
|
+
@admin_user = AdminUser.new(admin_user_params)
|
|
34
|
+
if @admin_user.save
|
|
35
|
+
redirect_to rsb_admin.admin_user_path(@admin_user), notice: 'Admin user created.'
|
|
36
|
+
else
|
|
37
|
+
render :new, status: :unprocessable_entity
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# GET /admin/admin_users/:id/edit
|
|
42
|
+
# Renders the edit form for an existing admin user.
|
|
43
|
+
def edit; end
|
|
44
|
+
|
|
45
|
+
# PATCH /admin/admin_users/:id
|
|
46
|
+
# Updates an existing admin user.
|
|
47
|
+
# Blank password fields are stripped so the existing password is not cleared.
|
|
48
|
+
def update
|
|
49
|
+
update_params = admin_user_params
|
|
50
|
+
if update_params[:password].blank?
|
|
51
|
+
update_params.delete(:password)
|
|
52
|
+
update_params.delete(:password_confirmation)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if @admin_user.update(update_params)
|
|
56
|
+
redirect_to rsb_admin.admin_user_path(@admin_user), notice: 'Admin user updated.'
|
|
57
|
+
else
|
|
58
|
+
render :edit, status: :unprocessable_entity
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# DELETE /admin/admin_users/:id
|
|
63
|
+
# Deletes an admin user. Self-deletion is prevented.
|
|
64
|
+
def destroy
|
|
65
|
+
if @admin_user == current_admin_user
|
|
66
|
+
redirect_to rsb_admin.admin_users_path, alert: 'You cannot delete your own account.'
|
|
67
|
+
return
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@admin_user.destroy!
|
|
71
|
+
redirect_to rsb_admin.admin_users_path, notice: 'Admin user deleted.'
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Builds breadcrumbs for admin user pages.
|
|
77
|
+
# Dashboard > System > Admin Users > #ID (if applicable) > New/Edit
|
|
78
|
+
#
|
|
79
|
+
# @return [void]
|
|
80
|
+
def build_breadcrumbs
|
|
81
|
+
super
|
|
82
|
+
add_breadcrumb(I18n.t('rsb.admin.shared.system'))
|
|
83
|
+
add_breadcrumb(I18n.t('rsb.admin.admin_users.title'), rsb_admin.admin_users_path)
|
|
84
|
+
add_breadcrumb("##{params[:id]}", rsb_admin.admin_user_path(params[:id])) if params[:id].present?
|
|
85
|
+
add_breadcrumb(I18n.t('rsb.admin.shared.new', resource: 'Admin User')) if action_name.in?(%w[new create])
|
|
86
|
+
return unless action_name.in?(%w[edit update])
|
|
87
|
+
|
|
88
|
+
add_breadcrumb(I18n.t('rsb.admin.shared.edit'))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Finds the admin user by ID from the URL params.
|
|
92
|
+
# @return [void]
|
|
93
|
+
def set_admin_user
|
|
94
|
+
@admin_user = AdminUser.find(params[:id])
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Permits the allowed admin user params from the request.
|
|
98
|
+
# @return [ActionController::Parameters]
|
|
99
|
+
def admin_user_params
|
|
100
|
+
params.require(:admin_user).permit(:email, :password, :password_confirmation, :role_id)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Authorizes the current admin user for admin_users actions.
|
|
104
|
+
# @return [void]
|
|
105
|
+
def authorize_admin_users
|
|
106
|
+
authorize_admin_action!(resource: 'admin_users', action: action_name)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSB
|
|
4
|
+
module Admin
|
|
5
|
+
class DashboardController < AdminController
|
|
6
|
+
before_action :authorize_dashboard, only: :index
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
@rsb_page_title = I18n.t('rsb.admin.dashboard.page_title', default: 'Dashboard')
|
|
10
|
+
dashboard_page = RSB::Admin.registry.dashboard_page
|
|
11
|
+
|
|
12
|
+
return unless dashboard_page
|
|
13
|
+
|
|
14
|
+
dispatch_to_dashboard(dashboard_page, :index)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def dashboard_action
|
|
18
|
+
dashboard_page = RSB::Admin.registry.dashboard_page
|
|
19
|
+
|
|
20
|
+
unless dashboard_page
|
|
21
|
+
head :not_found
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
action_key = params[:action_key]
|
|
26
|
+
action = dashboard_page.find_action(action_key)
|
|
27
|
+
|
|
28
|
+
unless action
|
|
29
|
+
head :not_found
|
|
30
|
+
return
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
authorize_admin_action!(resource: 'dashboard', action: action_key)
|
|
34
|
+
return if performed?
|
|
35
|
+
|
|
36
|
+
request.env['rsb.admin.breadcrumbs'] = @breadcrumbs
|
|
37
|
+
dispatch_to_dashboard(dashboard_page, action_key.to_sym)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Builds breadcrumbs for the dashboard page.
|
|
43
|
+
# Admin > Dashboard
|
|
44
|
+
#
|
|
45
|
+
# @return [void]
|
|
46
|
+
def build_breadcrumbs
|
|
47
|
+
super
|
|
48
|
+
add_breadcrumb(I18n.t('rsb.admin.dashboard.title'))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def authorize_dashboard
|
|
52
|
+
authorize_admin_action!(resource: 'dashboard', action: 'index')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Dispatch to a custom dashboard controller via Rack interface.
|
|
56
|
+
#
|
|
57
|
+
# Uses the same Rack dispatch pattern as ResourcesController for pages.
|
|
58
|
+
# Passes breadcrumbs via request.env and copies the response (status,
|
|
59
|
+
# headers, body) back to the current controller.
|
|
60
|
+
#
|
|
61
|
+
# @param dashboard_page [PageRegistration] the dashboard page registration
|
|
62
|
+
# @param action [Symbol] the action to invoke on the custom controller
|
|
63
|
+
# @return [void]
|
|
64
|
+
def dispatch_to_dashboard(dashboard_page, action)
|
|
65
|
+
request.env['rsb.admin.breadcrumbs'] = @breadcrumbs
|
|
66
|
+
controller_name = dashboard_page.controller
|
|
67
|
+
controller_class_name = "#{controller_name}_controller".classify
|
|
68
|
+
controller_class = controller_class_name.constantize
|
|
69
|
+
status, headers, body = controller_class.action(action).call(request.env)
|
|
70
|
+
self.status = status
|
|
71
|
+
self.response_body = body
|
|
72
|
+
headers.each { |k, v| response.headers[k] = v }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSB
|
|
4
|
+
module Admin
|
|
5
|
+
# Handles the admin user profile page where authenticated admins
|
|
6
|
+
# can view and edit their own email and password.
|
|
7
|
+
#
|
|
8
|
+
# Unlike other admin controllers, ProfileController does NOT perform
|
|
9
|
+
# RBAC authorization — any authenticated admin can access their profile.
|
|
10
|
+
# This is intentional: even admins with no role can change their
|
|
11
|
+
# credentials and sign out.
|
|
12
|
+
class ProfileController < AdminController
|
|
13
|
+
skip_before_action :require_admin_authentication, only: [:verify_email]
|
|
14
|
+
skip_before_action :build_breadcrumbs, only: [:verify_email]
|
|
15
|
+
skip_before_action :track_session_activity, only: [:verify_email]
|
|
16
|
+
# GET /admin/profile
|
|
17
|
+
# Displays the current admin user's profile information.
|
|
18
|
+
#
|
|
19
|
+
# @return [void]
|
|
20
|
+
def show
|
|
21
|
+
@rsb_page_title = I18n.t('rsb.admin.profile.show.page_title', default: 'Profile')
|
|
22
|
+
@admin_user = current_admin_user
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# GET /admin/profile/edit
|
|
26
|
+
# Renders the profile edit form for email and password changes.
|
|
27
|
+
#
|
|
28
|
+
# @return [void]
|
|
29
|
+
def edit
|
|
30
|
+
@rsb_page_title = I18n.t('rsb.admin.profile.edit.page_title', default: 'Edit Profile')
|
|
31
|
+
@admin_user = current_admin_user
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# PATCH /admin/profile
|
|
35
|
+
# Updates the current admin user's email and/or password.
|
|
36
|
+
# Requires current password confirmation for all changes.
|
|
37
|
+
#
|
|
38
|
+
# Email changes: stores new email as pending_email and sends verification.
|
|
39
|
+
# Password changes: updates immediately and destroys other sessions.
|
|
40
|
+
# If email unchanged: only password is updated (if provided).
|
|
41
|
+
#
|
|
42
|
+
# @return [void]
|
|
43
|
+
def update
|
|
44
|
+
@admin_user = current_admin_user
|
|
45
|
+
|
|
46
|
+
unless @admin_user.authenticate(params[:current_password].to_s)
|
|
47
|
+
flash.now[:alert] = I18n.t('rsb.admin.profile.password_incorrect')
|
|
48
|
+
render :edit, status: :unprocessable_entity
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
new_email = params[:admin_user][:email]&.strip&.downcase
|
|
53
|
+
email_changed = new_email.present? && new_email != @admin_user.email
|
|
54
|
+
|
|
55
|
+
# Handle password update
|
|
56
|
+
password_params = {}
|
|
57
|
+
if params[:admin_user][:password].present?
|
|
58
|
+
password_params[:password] = params[:admin_user][:password]
|
|
59
|
+
password_params[:password_confirmation] = params[:admin_user][:password_confirmation]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if password_params.any?
|
|
63
|
+
unless @admin_user.update(password_params)
|
|
64
|
+
render :edit, status: :unprocessable_entity
|
|
65
|
+
return
|
|
66
|
+
end
|
|
67
|
+
# Revoke all other sessions on password change (rule #15)
|
|
68
|
+
@admin_user.admin_sessions.where.not(session_token: session[:rsb_admin_session_token]).destroy_all
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Handle email change — initiate verification
|
|
72
|
+
if email_changed
|
|
73
|
+
begin
|
|
74
|
+
@admin_user.generate_email_verification!(new_email)
|
|
75
|
+
AdminMailer.email_verification(@admin_user).deliver_later
|
|
76
|
+
redirect_to rsb_admin.profile_path, notice: I18n.t('rsb.admin.profile.verification_sent')
|
|
77
|
+
rescue ActiveRecord::RecordInvalid
|
|
78
|
+
render :edit, status: :unprocessable_entity
|
|
79
|
+
end
|
|
80
|
+
else
|
|
81
|
+
redirect_to rsb_admin.profile_path, notice: I18n.t('rsb.admin.profile.updated')
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# GET /admin/profile/verify_email?token=...
|
|
86
|
+
# Confirms a pending email change if the token is valid and not expired.
|
|
87
|
+
# Does not require authentication — the token itself is the proof.
|
|
88
|
+
#
|
|
89
|
+
# @return [void]
|
|
90
|
+
def verify_email
|
|
91
|
+
admin = AdminUser.find_by(email_verification_token: params[:token])
|
|
92
|
+
|
|
93
|
+
if admin.nil?
|
|
94
|
+
redirect_to rsb_admin.login_path, alert: I18n.t('rsb.admin.profile.verification_invalid')
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if admin.email_verification_expired?
|
|
99
|
+
redirect_to rsb_admin.profile_path, alert: I18n.t('rsb.admin.profile.verification_expired')
|
|
100
|
+
return
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
admin.verify_email!
|
|
104
|
+
redirect_to rsb_admin.profile_path, notice: I18n.t('rsb.admin.profile.email_verified')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# POST /admin/profile/resend_verification
|
|
108
|
+
# Regenerates verification token and resends the verification email.
|
|
109
|
+
#
|
|
110
|
+
# @return [void]
|
|
111
|
+
def resend_verification
|
|
112
|
+
@admin_user = current_admin_user
|
|
113
|
+
|
|
114
|
+
if @admin_user.email_verification_pending?
|
|
115
|
+
@admin_user.generate_email_verification!(@admin_user.pending_email)
|
|
116
|
+
AdminMailer.email_verification(@admin_user).deliver_later
|
|
117
|
+
redirect_to rsb_admin.profile_path, notice: I18n.t('rsb.admin.profile.verification_resent')
|
|
118
|
+
else
|
|
119
|
+
redirect_to rsb_admin.profile_path
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
# Builds breadcrumbs for profile pages.
|
|
126
|
+
# Admin > Profile (show) or Admin > Profile > Edit (edit/update)
|
|
127
|
+
#
|
|
128
|
+
# @return [void]
|
|
129
|
+
def build_breadcrumbs
|
|
130
|
+
super
|
|
131
|
+
add_breadcrumb(I18n.t('rsb.admin.profile.title'), rsb_admin.profile_path)
|
|
132
|
+
return unless action_name.in?(%w[edit update])
|
|
133
|
+
|
|
134
|
+
add_breadcrumb(I18n.t('rsb.admin.shared.edit'))
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Permits the allowed profile params from the request.
|
|
138
|
+
# Note: role_id is NOT permitted — admins cannot change their own role.
|
|
139
|
+
#
|
|
140
|
+
# @return [ActionController::Parameters]
|
|
141
|
+
def profile_params
|
|
142
|
+
params.require(:admin_user).permit(:email, :password, :password_confirmation)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSB
|
|
4
|
+
module Admin
|
|
5
|
+
# Handles session revocation from the admin profile page.
|
|
6
|
+
#
|
|
7
|
+
# Like ProfileController, this does NOT perform RBAC authorization —
|
|
8
|
+
# any authenticated admin can manage their own sessions.
|
|
9
|
+
class ProfileSessionsController < AdminController
|
|
10
|
+
# DELETE /admin/profile/sessions/:id
|
|
11
|
+
# Revokes a single session belonging to the current admin user.
|
|
12
|
+
# Cannot revoke the current session (safety check).
|
|
13
|
+
#
|
|
14
|
+
# @return [void]
|
|
15
|
+
def destroy
|
|
16
|
+
admin_session = current_admin_user.admin_sessions.find_by(id: params[:id])
|
|
17
|
+
|
|
18
|
+
if admin_session.nil?
|
|
19
|
+
redirect_to rsb_admin.profile_path, alert: I18n.t('rsb.admin.profile.session_not_found')
|
|
20
|
+
return
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
if admin_session.current?(session[:rsb_admin_session_token])
|
|
24
|
+
redirect_to rsb_admin.profile_path, alert: I18n.t('rsb.admin.profile.cannot_revoke_current')
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
admin_session.destroy
|
|
29
|
+
redirect_to rsb_admin.profile_path, notice: I18n.t('rsb.admin.profile.session_revoked')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# DELETE /admin/profile/sessions
|
|
33
|
+
# Revokes all sessions except the current one.
|
|
34
|
+
#
|
|
35
|
+
# @return [void]
|
|
36
|
+
def destroy_all
|
|
37
|
+
current_token = session[:rsb_admin_session_token]
|
|
38
|
+
count = current_admin_user.admin_sessions.where.not(session_token: current_token).count
|
|
39
|
+
current_admin_user.admin_sessions.where.not(session_token: current_token).destroy_all
|
|
40
|
+
|
|
41
|
+
redirect_to rsb_admin.profile_path, notice: I18n.t('rsb.admin.profile.all_sessions_revoked', count: count)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|