pg_sql_triggers 1.2.0 → 1.3.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +144 -0
  3. data/COVERAGE.md +26 -19
  4. data/Goal.md +276 -155
  5. data/README.md +27 -1
  6. data/app/assets/javascripts/pg_sql_triggers/trigger_actions.js +50 -0
  7. data/app/controllers/concerns/pg_sql_triggers/error_handling.rb +56 -0
  8. data/app/controllers/concerns/pg_sql_triggers/kill_switch_protection.rb +66 -0
  9. data/app/controllers/concerns/pg_sql_triggers/permission_checking.rb +117 -0
  10. data/app/controllers/pg_sql_triggers/application_controller.rb +10 -62
  11. data/app/controllers/pg_sql_triggers/audit_logs_controller.rb +102 -0
  12. data/app/controllers/pg_sql_triggers/dashboard_controller.rb +4 -9
  13. data/app/controllers/pg_sql_triggers/tables_controller.rb +30 -4
  14. data/app/controllers/pg_sql_triggers/triggers_controller.rb +3 -21
  15. data/app/helpers/pg_sql_triggers/permissions_helper.rb +43 -0
  16. data/app/models/pg_sql_triggers/audit_log.rb +106 -0
  17. data/app/models/pg_sql_triggers/trigger_registry.rb +178 -9
  18. data/app/views/layouts/pg_sql_triggers/application.html.erb +26 -6
  19. data/app/views/pg_sql_triggers/audit_logs/index.html.erb +177 -0
  20. data/app/views/pg_sql_triggers/dashboard/index.html.erb +33 -8
  21. data/app/views/pg_sql_triggers/tables/index.html.erb +76 -3
  22. data/app/views/pg_sql_triggers/tables/show.html.erb +17 -4
  23. data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +16 -7
  24. data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +16 -7
  25. data/app/views/pg_sql_triggers/triggers/show.html.erb +26 -6
  26. data/config/routes.rb +2 -0
  27. data/db/migrate/20260103000001_create_pg_sql_triggers_audit_log.rb +28 -0
  28. data/docs/README.md +15 -5
  29. data/docs/api-reference.md +191 -0
  30. data/docs/audit-trail.md +413 -0
  31. data/docs/configuration.md +6 -6
  32. data/docs/permissions.md +369 -0
  33. data/docs/troubleshooting.md +486 -0
  34. data/docs/ui-guide.md +211 -0
  35. data/docs/web-ui.md +257 -34
  36. data/lib/pg_sql_triggers/errors.rb +245 -0
  37. data/lib/pg_sql_triggers/generator/service.rb +32 -0
  38. data/lib/pg_sql_triggers/permissions/checker.rb +9 -2
  39. data/lib/pg_sql_triggers/registry.rb +141 -8
  40. data/lib/pg_sql_triggers/sql/kill_switch.rb +33 -5
  41. data/lib/pg_sql_triggers/testing/function_tester.rb +2 -0
  42. data/lib/pg_sql_triggers/version.rb +1 -1
  43. data/lib/pg_sql_triggers.rb +3 -6
  44. metadata +29 -6
  45. data/docs/screenshots/.gitkeep +0 -1
  46. data/docs/screenshots/Generate Trigger.png +0 -0
  47. data/docs/screenshots/Triggers Page.png +0 -0
  48. data/docs/screenshots/kill error.png +0 -0
  49. data/docs/screenshots/kill modal for migration down.png +0 -0
@@ -0,0 +1,50 @@
1
+ // AJAX handlers for trigger actions
2
+ (function() {
3
+ 'use strict';
4
+
5
+ // Handle AJAX form submissions
6
+ document.addEventListener('DOMContentLoaded', function() {
7
+ // Set up AJAX error handling
8
+ document.addEventListener('ajax:error', function(event) {
9
+ const detail = event.detail || [];
10
+ const error = detail[0] || {};
11
+ const status = error.status || 500;
12
+ const message = error.message || 'An error occurred';
13
+
14
+ alert('Error: ' + message);
15
+ console.error('AJAX Error:', error);
16
+ });
17
+
18
+ // Handle AJAX success for trigger actions
19
+ document.addEventListener('ajax:success', function(event) {
20
+ const [data, status, xhr] = event.detail;
21
+
22
+ // If the response contains a redirect, follow it
23
+ if (xhr.getResponseHeader('Location')) {
24
+ window.location.href = xhr.getResponseHeader('Location');
25
+ } else {
26
+ // Otherwise, reload the page to show updated state
27
+ window.location.reload();
28
+ }
29
+ });
30
+
31
+ // Handle AJAX complete to show loading states
32
+ document.addEventListener('ajax:before', function(event) {
33
+ const form = event.target;
34
+ const submitButton = form.querySelector('button[type="submit"], button[type="button"]');
35
+ if (submitButton) {
36
+ submitButton.disabled = true;
37
+ submitButton.textContent = 'Processing...';
38
+ }
39
+ });
40
+
41
+ document.addEventListener('ajax:complete', function(event) {
42
+ const form = event.target;
43
+ const submitButton = form.querySelector('button[type="submit"], button[type="button"]');
44
+ if (submitButton) {
45
+ submitButton.disabled = false;
46
+ }
47
+ });
48
+ });
49
+ })();
50
+
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgSqlTriggers
4
+ module ErrorHandling
5
+ extend ActiveSupport::Concern
6
+
7
+ # Handles errors and formats them for display.
8
+ # Returns a formatted error message for flash display.
9
+ #
10
+ # @param error [Exception] The error to format
11
+ # @return [String] Formatted error message
12
+ def format_error_for_flash(error)
13
+ return error.to_s unless error.is_a?(PgSqlTriggers::Error)
14
+
15
+ # Use user_message which includes recovery suggestions
16
+ error.user_message
17
+ end
18
+
19
+ # Rescues from PgSqlTriggers errors and sets appropriate flash messages.
20
+ #
21
+ # @param error [Exception] The error to handle
22
+ # @return [void]
23
+ def rescue_pg_sql_triggers_error(error)
24
+ Rails.logger.error("#{error.class.name}: #{error.message}")
25
+ Rails.logger.error(error.backtrace.join("\n")) if Rails.env.development? && error.respond_to?(:backtrace)
26
+
27
+ flash[:error] = if error.is_a?(PgSqlTriggers::Error)
28
+ format_error_for_flash(error)
29
+ else
30
+ "An unexpected error occurred: #{error.message}"
31
+ end
32
+ end
33
+
34
+ # Handles kill switch errors with appropriate flash message and redirect.
35
+ #
36
+ # @param error [PgSqlTriggers::KillSwitchError] The kill switch error
37
+ # @param redirect_path [String, nil] Optional redirect path (defaults to root_path)
38
+ # @return [void]
39
+ def handle_kill_switch_error(error, redirect_path: nil)
40
+ flash[:error] = error.message
41
+ redirect_to redirect_path || root_path
42
+ end
43
+
44
+ # Handles standard errors with logging and flash message.
45
+ #
46
+ # @param error [Exception] The error to handle
47
+ # @param operation [String] Description of the operation that failed
48
+ # @param redirect_path [String, nil] Optional redirect path (defaults to root_path)
49
+ # @return [void]
50
+ def handle_standard_error(error, operation:, redirect_path: nil)
51
+ Rails.logger.error("#{operation} failed: #{error.message}\n#{error.backtrace.join("\n")}")
52
+ flash[:error] = "#{operation}: #{error.message}"
53
+ redirect_to redirect_path || root_path
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgSqlTriggers
4
+ module KillSwitchProtection
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # Helper method available in views
9
+ helper_method :kill_switch_active?, :expected_confirmation_text, :current_environment
10
+ end
11
+
12
+ # Checks if kill switch is active for the current environment.
13
+ #
14
+ # @return [Boolean] true if kill switch is active, false otherwise
15
+ def kill_switch_active?
16
+ PgSqlTriggers::SQL::KillSwitch.active?(environment: current_environment)
17
+ end
18
+
19
+ # Checks kill switch before executing a dangerous operation.
20
+ # Raises KillSwitchError if the operation is blocked.
21
+ #
22
+ # @param operation [Symbol] The operation being performed
23
+ # @param confirmation [String, nil] Optional confirmation text from params
24
+ # @raise [PgSqlTriggers::KillSwitchError] If the operation is blocked
25
+ # @return [true] If the operation is allowed
26
+ def check_kill_switch(operation:, confirmation: nil)
27
+ PgSqlTriggers::SQL::KillSwitch.check!(
28
+ operation: operation,
29
+ environment: current_environment,
30
+ confirmation: confirmation,
31
+ actor: current_actor
32
+ )
33
+ end
34
+
35
+ # Before action to require kill switch override for an action.
36
+ # Add to specific controller actions that need protection:
37
+ # before_action -> { require_kill_switch_override(:operation_name) }, only: [:dangerous_action]
38
+ #
39
+ # @param operation [Symbol] The operation name
40
+ # @param confirmation [String, nil] Optional confirmation text
41
+ # @raise [PgSqlTriggers::KillSwitchError] If the operation is blocked
42
+ def require_kill_switch_override(operation, confirmation: nil)
43
+ check_kill_switch(operation: operation, confirmation: confirmation)
44
+ end
45
+
46
+ # Returns the expected confirmation text for an operation (for use in views).
47
+ #
48
+ # @param operation [Symbol] The operation name
49
+ # @return [String] The expected confirmation text
50
+ def expected_confirmation_text(operation)
51
+ if PgSqlTriggers.respond_to?(:kill_switch_confirmation_pattern) &&
52
+ PgSqlTriggers.kill_switch_confirmation_pattern.respond_to?(:call)
53
+ PgSqlTriggers.kill_switch_confirmation_pattern.call(operation)
54
+ else
55
+ "EXECUTE #{operation.to_s.upcase}"
56
+ end
57
+ end
58
+
59
+ # Returns the current environment.
60
+ #
61
+ # @return [String] The current Rails environment
62
+ def current_environment
63
+ Rails.env
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgSqlTriggers
4
+ module PermissionChecking
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # Helper methods available in views
9
+ helper_method :current_actor, :can_view_triggers?, :can_enable_disable_triggers?,
10
+ :can_drop_triggers?, :can_execute_sql?, :can_generate_triggers?, :can_apply_triggers?
11
+ end
12
+
13
+ # Returns the current actor (user) performing the action.
14
+ # Override this method in host application to provide actual user.
15
+ #
16
+ # @return [Hash] Actor information with :type and :id keys
17
+ def current_actor
18
+ {
19
+ type: current_user_type,
20
+ id: current_user_id
21
+ }
22
+ end
23
+
24
+ # Returns the current user type.
25
+ # Override this method in host application.
26
+ #
27
+ # @return [String] User type (default: "User")
28
+ def current_user_type
29
+ "User"
30
+ end
31
+
32
+ # Returns the current user ID.
33
+ # Override this method in host application.
34
+ #
35
+ # @return [String] User ID (default: "unknown")
36
+ def current_user_id
37
+ "unknown"
38
+ end
39
+
40
+ # Checks if current actor has viewer permissions.
41
+ #
42
+ # @raise [ActionController::RedirectError] Redirects if permission denied
43
+ def check_viewer_permission
44
+ can_access = begin
45
+ PgSqlTriggers::Permissions.can?(current_actor, :view_triggers, environment: current_environment)
46
+ rescue StandardError => e
47
+ Rails.logger.error("Permission check failed: #{e.message}\n#{e.backtrace.join("\n")}")
48
+ false
49
+ end
50
+ return if can_access
51
+
52
+ redirect_to root_path, alert: "Insufficient permissions. Viewer role required."
53
+ end
54
+
55
+ # Checks if current actor has operator permissions (enable/disable/apply).
56
+ #
57
+ # @raise [ActionController::RedirectError] Redirects if permission denied
58
+ def check_operator_permission
59
+ can_access = begin
60
+ PgSqlTriggers::Permissions.can?(current_actor, :enable_trigger, environment: current_environment)
61
+ rescue StandardError => e
62
+ Rails.logger.error("Permission check failed: #{e.message}\n#{e.backtrace.join("\n")}")
63
+ false
64
+ end
65
+ return if can_access
66
+
67
+ redirect_to root_path, alert: "Insufficient permissions. Operator role required."
68
+ end
69
+
70
+ # Checks if current actor has admin permissions (drop/re-execute/execute SQL).
71
+ #
72
+ # @raise [ActionController::RedirectError] Redirects if permission denied
73
+ def check_admin_permission
74
+ can_access = begin
75
+ PgSqlTriggers::Permissions.can?(current_actor, :drop_trigger, environment: current_environment)
76
+ rescue StandardError => e
77
+ Rails.logger.error("Permission check failed: #{e.message}\n#{e.backtrace.join("\n")}")
78
+ false
79
+ end
80
+ return if can_access
81
+
82
+ redirect_to root_path, alert: "Insufficient permissions. Admin role required."
83
+ end
84
+
85
+ # Permission helper methods for views
86
+
87
+ # @return [Boolean] true if current actor can view triggers
88
+ def can_view_triggers?
89
+ PgSqlTriggers::Permissions.can?(current_actor, :view_triggers, environment: current_environment)
90
+ end
91
+
92
+ # @return [Boolean] true if current actor can enable/disable triggers
93
+ def can_enable_disable_triggers?
94
+ PgSqlTriggers::Permissions.can?(current_actor, :enable_trigger, environment: current_environment)
95
+ end
96
+
97
+ # @return [Boolean] true if current actor can drop triggers
98
+ def can_drop_triggers?
99
+ PgSqlTriggers::Permissions.can?(current_actor, :drop_trigger, environment: current_environment)
100
+ end
101
+
102
+ # @return [Boolean] true if current actor can execute SQL capsules
103
+ def can_execute_sql?
104
+ PgSqlTriggers::Permissions.can?(current_actor, :execute_sql, environment: current_environment)
105
+ end
106
+
107
+ # @return [Boolean] true if current actor can generate triggers
108
+ def can_generate_triggers?
109
+ PgSqlTriggers::Permissions.can?(current_actor, :apply_trigger, environment: current_environment)
110
+ end
111
+
112
+ # @return [Boolean] true if current actor can apply triggers
113
+ def can_apply_triggers?
114
+ PgSqlTriggers::Permissions.can?(current_actor, :apply_trigger, environment: current_environment)
115
+ end
116
+ end
117
+ end
@@ -1,81 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgSqlTriggers
4
+ # Base controller for all pg_sql_triggers controllers.
5
+ # Includes common concerns for kill switch protection, permission checking, and error handling.
4
6
  class ApplicationController < ActionController::Base
5
7
  include PgSqlTriggers::Engine.routes.url_helpers
8
+ include PgSqlTriggers::KillSwitchProtection
9
+ include PgSqlTriggers::PermissionChecking
10
+ include PgSqlTriggers::ErrorHandling
6
11
 
7
12
  protect_from_forgery with: :exception
8
13
  layout "pg_sql_triggers/application"
9
14
 
10
15
  before_action :check_permissions?
11
16
 
12
- # Helper methods available in views
13
- helper_method :current_environment, :kill_switch_active?, :expected_confirmation_text, :current_actor
17
+ # Include permissions helper for view helpers
18
+ include PgSqlTriggers::PermissionsHelper
14
19
 
15
20
  private
16
21
 
22
+ # Override this method in host application to implement custom permission checks.
23
+ #
24
+ # @return [Boolean] true if permissions check passes
17
25
  def check_permissions?
18
- # Override this method in host application to implement custom permission checks
19
26
  true
20
27
  end
21
-
22
- def current_actor
23
- # Override this in host application to provide actual user
24
- {
25
- type: current_user_type,
26
- id: current_user_id
27
- }
28
- end
29
-
30
- def current_user_type
31
- "User"
32
- end
33
-
34
- def current_user_id
35
- "unknown"
36
- end
37
-
38
- # ========== Kill Switch Helpers ==========
39
-
40
- # Returns the current environment
41
- def current_environment
42
- Rails.env
43
- end
44
-
45
- # Checks if kill switch is active for the current environment
46
- def kill_switch_active?
47
- PgSqlTriggers::SQL::KillSwitch.active?(environment: current_environment)
48
- end
49
-
50
- # Checks kill switch before executing a dangerous operation
51
- # Raises KillSwitchError if the operation is blocked
52
- #
53
- # @param operation [Symbol] The operation being performed
54
- # @param confirmation [String, nil] Optional confirmation text from params
55
- def check_kill_switch(operation:, confirmation: nil)
56
- PgSqlTriggers::SQL::KillSwitch.check!(
57
- operation: operation,
58
- environment: current_environment,
59
- confirmation: confirmation,
60
- actor: current_actor
61
- )
62
- end
63
-
64
- # Before action to require kill switch override for an action
65
- # Add to specific controller actions that need protection:
66
- # before_action -> { require_kill_switch_override(:operation_name) }, only: [:dangerous_action]
67
- def require_kill_switch_override(operation, confirmation: nil)
68
- check_kill_switch(operation: operation, confirmation: confirmation)
69
- end
70
-
71
- # Returns the expected confirmation text for an operation (for use in views)
72
- def expected_confirmation_text(operation)
73
- if PgSqlTriggers.respond_to?(:kill_switch_confirmation_pattern) &&
74
- PgSqlTriggers.kill_switch_confirmation_pattern.respond_to?(:call)
75
- PgSqlTriggers.kill_switch_confirmation_pattern.call(operation)
76
- else
77
- "EXECUTE #{operation.to_s.upcase}"
78
- end
79
- end
80
28
  end
81
29
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgSqlTriggers
4
+ class AuditLogsController < ApplicationController
5
+ before_action :check_viewer_permission
6
+
7
+ # GET /audit_logs
8
+ # Display audit log entries with filtering and sorting
9
+ def index
10
+ @audit_logs = PgSqlTriggers::AuditLog.all
11
+
12
+ # Filter by trigger name
13
+ @audit_logs = @audit_logs.for_trigger(params[:trigger_name]) if params[:trigger_name].present?
14
+
15
+ # Filter by operation
16
+ @audit_logs = @audit_logs.for_operation(params[:operation]) if params[:operation].present?
17
+
18
+ # Filter by status
19
+ if params[:status].present? && %w[success failure].include?(params[:status])
20
+ @audit_logs = @audit_logs.where(status: params[:status])
21
+ end
22
+
23
+ # Filter by environment
24
+ @audit_logs = @audit_logs.for_environment(params[:environment]) if params[:environment].present?
25
+
26
+ # Filter by actor (search in JSONB field)
27
+ @audit_logs = @audit_logs.where("actor->>'id' = ?", params[:actor_id]) if params[:actor_id].present?
28
+
29
+ # Sort by date (default: most recent first)
30
+ sort_direction = params[:sort] == "asc" ? :asc : :desc
31
+ @audit_logs = @audit_logs.order(created_at: sort_direction)
32
+
33
+ # Pagination
34
+ @per_page = (params[:per_page] || 50).to_i
35
+ @per_page = [@per_page, 200].min # Cap at 200
36
+ @page = (params[:page] || 1).to_i
37
+ @total_count = @audit_logs.count
38
+ @total_pages = @total_count.positive? ? (@total_count.to_f / @per_page).ceil : 1
39
+ @page = @page.clamp(1, @total_pages)
40
+
41
+ offset = (@page - 1) * @per_page
42
+ @audit_logs = @audit_logs.offset(offset).limit(@per_page)
43
+
44
+ # Get distinct values for filter dropdowns
45
+ @available_trigger_names = PgSqlTriggers::AuditLog.distinct.pluck(:trigger_name).compact.sort
46
+ @available_operations = PgSqlTriggers::AuditLog.distinct.pluck(:operation).compact.sort
47
+ @available_environments = PgSqlTriggers::AuditLog.distinct.pluck(:environment).compact.sort
48
+
49
+ respond_to do |format|
50
+ format.html
51
+ format.csv do
52
+ send_data generate_csv, filename: "audit_logs_#{Time.current.strftime('%Y%m%d_%H%M%S')}.csv",
53
+ type: "text/csv", disposition: "attachment"
54
+ end
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def generate_csv
61
+ require "csv"
62
+
63
+ # Get all audit logs (no pagination for CSV)
64
+ audit_logs = PgSqlTriggers::AuditLog.all
65
+
66
+ # Apply filters
67
+ audit_logs = audit_logs.for_trigger(params[:trigger_name]) if params[:trigger_name].present?
68
+ audit_logs = audit_logs.for_operation(params[:operation]) if params[:operation].present?
69
+ if params[:status].present? && %w[success failure].include?(params[:status])
70
+ audit_logs = audit_logs.where(status: params[:status])
71
+ end
72
+ audit_logs = audit_logs.for_environment(params[:environment]) if params[:environment].present?
73
+ audit_logs = audit_logs.where("actor->>'id' = ?", params[:actor_id]) if params[:actor_id].present?
74
+
75
+ CSV.generate(headers: true) do |csv|
76
+ csv << [
77
+ "ID", "Trigger Name", "Operation", "Status", "Environment",
78
+ "Actor Type", "Actor ID", "Reason", "Error Message",
79
+ "Created At"
80
+ ]
81
+
82
+ audit_logs.order(created_at: :desc).find_each do |log|
83
+ actor_type = log.actor.is_a?(Hash) ? log.actor["type"] || log.actor[:type] : nil
84
+ actor_id = log.actor.is_a?(Hash) ? log.actor["id"] || log.actor[:id] : nil
85
+
86
+ csv << [
87
+ log.id,
88
+ log.trigger_name || "",
89
+ log.operation,
90
+ log.status,
91
+ log.environment || "",
92
+ actor_type || "",
93
+ actor_id || "",
94
+ log.reason || "",
95
+ log.error_message || "",
96
+ log.created_at&.iso8601 || ""
97
+ ]
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -5,7 +5,10 @@ module PgSqlTriggers
5
5
  before_action :check_viewer_permission
6
6
 
7
7
  def index
8
- @triggers = PgSqlTriggers::TriggerRegistry.order(created_at: :desc)
8
+ # Sort by installed_at descending (most recent first), fallback to created_at
9
+ @triggers = PgSqlTriggers::TriggerRegistry.order(
10
+ Arel.sql("COALESCE(installed_at, created_at) DESC")
11
+ )
9
12
 
10
13
  # Get drift summary
11
14
  drift_summary = PgSqlTriggers::Drift::Reporter.summary
@@ -43,13 +46,5 @@ module PgSqlTriggers
43
46
  @per_page = 20
44
47
  end
45
48
  end
46
-
47
- private
48
-
49
- def check_viewer_permission
50
- return if PgSqlTriggers::Permissions.can?(current_actor, :view_triggers)
51
-
52
- redirect_to root_path, alert: "Insufficient permissions. Viewer role required."
53
- end
54
49
  end
55
50
  end
@@ -2,12 +2,38 @@
2
2
 
3
3
  module PgSqlTriggers
4
4
  class TablesController < ApplicationController
5
+ before_action :check_viewer_permission
6
+
5
7
  def index
6
8
  all_tables = PgSqlTriggers::DatabaseIntrospection.new.tables_with_triggers
7
- # Only show tables that have at least one trigger
8
- @tables_with_triggers = all_tables.select { |t| t[:trigger_count].positive? }
9
- @total_tables = @tables_with_triggers.count
10
- @tables_with_trigger_count = @tables_with_triggers.count
9
+
10
+ # Calculate statistics
11
+ @tables_with_trigger_count = all_tables.count { |t| t[:trigger_count].positive? }
12
+ @tables_without_trigger_count = all_tables.count { |t| t[:trigger_count].zero? }
13
+ @total_tables_count = all_tables.count
14
+
15
+ # Filter based on parameter
16
+ @filter = params[:filter] || "with_triggers"
17
+ filtered_tables = case @filter
18
+ when "with_triggers"
19
+ all_tables.select { |t| t[:trigger_count].positive? }
20
+ when "without_triggers"
21
+ all_tables.select { |t| t[:trigger_count].zero? }
22
+ else # 'all'
23
+ all_tables
24
+ end
25
+
26
+ @total_tables = filtered_tables.count
27
+
28
+ # Pagination
29
+ @per_page = (params[:per_page] || 20).to_i
30
+ @per_page = [@per_page, 100].min # Cap at 100
31
+ @page = (params[:page] || 1).to_i
32
+ @total_pages = @total_tables.positive? ? (@total_tables.to_f / @per_page).ceil : 1
33
+ @page = @page.clamp(1, @total_pages) # Ensure page is within valid range
34
+
35
+ offset = (@page - 1) * @per_page
36
+ @tables_with_triggers = filtered_tables.slice(offset, @per_page) || []
11
37
  end
12
38
 
13
39
  def show
@@ -4,10 +4,10 @@ module PgSqlTriggers
4
4
  # Controller for managing individual triggers via web UI
5
5
  # Provides actions to enable and disable triggers
6
6
  class TriggersController < ApplicationController
7
- before_action :set_trigger, only: %i[show enable disable drop re_execute]
8
7
  before_action :check_viewer_permission, only: [:show]
9
8
  before_action :check_operator_permission, only: %i[enable disable]
10
9
  before_action :check_admin_permission, only: %i[drop re_execute]
10
+ before_action :set_trigger, only: %i[show enable disable drop re_execute]
11
11
 
12
12
  def show
13
13
  # Load trigger details and drift information
@@ -18,7 +18,7 @@ module PgSqlTriggers
18
18
  # Check kill switch before enabling trigger
19
19
  check_kill_switch(operation: :ui_trigger_enable, confirmation: params[:confirmation_text])
20
20
 
21
- @trigger.enable!(confirmation: params[:confirmation_text])
21
+ @trigger.enable!(confirmation: params[:confirmation_text], actor: current_actor)
22
22
  flash[:success] = "Trigger '#{@trigger.trigger_name}' enabled successfully."
23
23
  redirect_to redirect_path
24
24
  rescue PgSqlTriggers::KillSwitchError => e
@@ -34,7 +34,7 @@ module PgSqlTriggers
34
34
  # Check kill switch before disabling trigger
35
35
  check_kill_switch(operation: :ui_trigger_disable, confirmation: params[:confirmation_text])
36
36
 
37
- @trigger.disable!(confirmation: params[:confirmation_text])
37
+ @trigger.disable!(confirmation: params[:confirmation_text], actor: current_actor)
38
38
  flash[:success] = "Trigger '#{@trigger.trigger_name}' disabled successfully."
39
39
  redirect_to redirect_path
40
40
  rescue PgSqlTriggers::KillSwitchError => e
@@ -119,24 +119,6 @@ module PgSqlTriggers
119
119
  redirect_to root_path
120
120
  end
121
121
 
122
- def check_viewer_permission
123
- return if PgSqlTriggers::Permissions.can?(current_actor, :view_triggers)
124
-
125
- redirect_to root_path, alert: "Insufficient permissions. Viewer role required."
126
- end
127
-
128
- def check_operator_permission
129
- return if PgSqlTriggers::Permissions.can?(current_actor, :enable_trigger)
130
-
131
- redirect_to root_path, alert: "Insufficient permissions. Operator role required."
132
- end
133
-
134
- def check_admin_permission
135
- return if PgSqlTriggers::Permissions.can?(current_actor, :drop_trigger)
136
-
137
- redirect_to root_path, alert: "Insufficient permissions. Admin role required."
138
- end
139
-
140
122
  def redirect_path
141
123
  # Redirect back to the referring page if possible, otherwise to dashboard
142
124
  params[:redirect_to].presence || request.referer || root_path
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgSqlTriggers
4
+ module PermissionsHelper
5
+ # Check if the current actor can perform an action
6
+ #
7
+ # @param action [Symbol, String] The action to check
8
+ # @return [Boolean] True if the actor can perform the action
9
+ def can?(action)
10
+ PgSqlTriggers::Permissions.can?(current_actor, action, environment: current_environment)
11
+ end
12
+
13
+ # Check if the current actor can view triggers
14
+ def can_view_triggers?
15
+ can?(:view_triggers)
16
+ end
17
+
18
+ # Check if the current actor can enable/disable triggers
19
+ def can_enable_disable_triggers?
20
+ can?(:enable_trigger)
21
+ end
22
+
23
+ # Check if the current actor can drop triggers
24
+ def can_drop_triggers?
25
+ can?(:drop_trigger)
26
+ end
27
+
28
+ # Check if the current actor can execute SQL capsules
29
+ def can_execute_sql?
30
+ can?(:execute_sql)
31
+ end
32
+
33
+ # Check if the current actor can generate triggers
34
+ def can_generate_triggers?
35
+ can?(:generate_trigger)
36
+ end
37
+
38
+ # Check if the current actor can apply triggers (run migrations)
39
+ def can_apply_triggers?
40
+ can?(:apply_trigger)
41
+ end
42
+ end
43
+ end