diff --git a/.env.example b/.env.example index e199dac..680a8ca 100644 --- a/.env.example +++ b/.env.example @@ -68,3 +68,27 @@ CLINCH_ALLOW_LOCALHOST=true # Optional: Set custom port # PORT=9000 + +# Sentry Configuration (Optional) +# Enable error tracking and performance monitoring +# Leave SENTRY_DSN empty to disable Sentry completely +# +# Production: Get your DSN from https://sentry.io/settings/projects/ +# SENTRY_DSN=https://your-dsn@sentry.io/project-id +# +# Optional: Override Sentry environment (defaults to Rails.env) +# SENTRY_ENVIRONMENT=production +# +# Optional: Override Sentry release (defaults to Git commit hash) +# SENTRY_RELEASE=v1.0.0 +# +# Optional: Performance monitoring sample rate (0.0 to 1.0, default 0.2) +# Higher values provide more data but cost more +# SENTRY_TRACES_SAMPLE_RATE=0.2 +# +# Optional: Continuous profiling sample rate (0.0 to 1.0, default 0.0) +# Very resource intensive, only enable for performance investigations +# SENTRY_PROFILES_SAMPLE_RATE=0.0 +# +# Development: Enable Sentry in development for testing +# SENTRY_ENABLED_IN_DEVELOPMENT=true diff --git a/Gemfile.lock b/Gemfile.lock index e5c8528..1facf4a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -333,6 +333,12 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) + sentry-rails (5.28.0) + railties (>= 5.0) + sentry-ruby (~> 5.28.0) + sentry-ruby (5.28.0) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) solid_cable (3.0.12) actioncable (>= 7.2) activejob (>= 7.2) @@ -443,6 +449,8 @@ DEPENDENCIES rqrcode (~> 3.1) rubocop-rails-omakase selenium-webdriver + sentry-rails (~> 5.18) + sentry-ruby (~> 5.18) solid_cable solid_cache sqlite3 (>= 2.1) diff --git a/app/controllers/api/csp_controller.rb b/app/controllers/api/csp_controller.rb index 6f257fa..0a8cdab 100644 --- a/app/controllers/api/csp_controller.rb +++ b/app/controllers/api/csp_controller.rb @@ -8,19 +8,38 @@ module Api def violation_report # Parse CSP violation report report_data = JSON.parse(request.body.read) + csp_report = report_data['csp-report'] # Log the violation for security monitoring Rails.logger.warn "CSP Violation Report:" - Rails.logger.warn " Blocked URI: #{report_data.dig('csp-report', 'blocked-uri')}" - Rails.logger.warn " Document URI: #{report_data.dig('csp-report', 'document-uri')}" - Rails.logger.warn " Referrer: #{report_data.dig('csp-report', 'referrer')}" - Rails.logger.warn " Violated Directive: #{report_data.dig('csp-report', 'violated-directive')}" - Rails.logger.warn " Original Policy: #{report_data.dig('csp-report', 'original-policy')}" + Rails.logger.warn " Blocked URI: #{csp_report['blocked-uri']}" + Rails.logger.warn " Document URI: #{csp_report['document-uri']}" + Rails.logger.warn " Referrer: #{csp_report['referrer']}" + Rails.logger.warn " Violated Directive: #{csp_report['violated-directive']}" + Rails.logger.warn " Original Policy: #{csp_report['original-policy']}" Rails.logger.warn " User Agent: #{request.user_agent}" Rails.logger.warn " IP Address: #{request.remote_ip}" - # In production, you might want to send this to a security monitoring service - # For now, we'll just log it and return a success response + # Emit structured event for CSP violation + # This allows multiple subscribers to process the event (Sentry, local logging, etc.) + Rails.event.notify("csp.violation", { + blocked_uri: csp_report['blocked-uri'], + document_uri: csp_report['document-uri'], + referrer: csp_report['referrer'], + violated_directive: csp_report['violated-directive'], + original_policy: csp_report['original-policy'], + disposition: csp_report['disposition'], + effective_directive: csp_report['effective-directive'], + source_file: csp_report['source-file'], + line_number: csp_report['line-number'], + column_number: csp_report['column-number'], + status_code: csp_report['status-code'], + user_agent: request.user_agent, + ip_address: request.remote_ip, + current_user_id: Current.user&.id, + timestamp: Time.current, + session_id: Current.session&.id + }) head :no_content rescue JSON::ParserError => e diff --git a/app/javascript/controllers/mobile_sidebar_controller.js b/app/javascript/controllers/mobile_sidebar_controller.js index 4d8c491..f390dd6 100644 --- a/app/javascript/controllers/mobile_sidebar_controller.js +++ b/app/javascript/controllers/mobile_sidebar_controller.js @@ -1,21 +1,48 @@ import { Controller } from "@hotwired/stimulus"; export default class extends Controller { - static targets = ["sidebarOverlay", "button"]; + static targets = ["sidebarOverlay"]; connect() { // Initialize mobile sidebar functionality + // Add escape key listener to close sidebar + this.boundHandleEscape = this.handleEscape.bind(this); + document.addEventListener('keydown', this.boundHandleEscape); + } + + disconnect() { + // Clean up event listeners + document.removeEventListener('keydown', this.boundHandleEscape); } openSidebar() { if (this.hasSidebarOverlayTarget) { this.sidebarOverlayTarget.classList.remove('hidden'); + // Prevent body scroll when sidebar is open + document.body.style.overflow = 'hidden'; } } closeSidebar() { if (this.hasSidebarOverlayTarget) { this.sidebarOverlayTarget.classList.add('hidden'); + // Restore body scroll + document.body.style.overflow = ''; + } + } + + // Close sidebar when clicking on the overlay background + closeOnBackgroundClick(event) { + // Check if the click is on the overlay background (the semi-transparent layer) + if (event.target === this.sidebarOverlayTarget || event.target.classList.contains('bg-gray-900/80')) { + this.closeSidebar(); + } + } + + // Handle escape key to close sidebar + handleEscape(event) { + if (event.key === 'Escape' && this.hasSidebarOverlayTarget && !this.sidebarOverlayTarget.classList.contains('hidden')) { + this.closeSidebar(); } } } \ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 6d1f441..31b7ff6 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -25,27 +25,29 @@ <% if authenticated? %> - <%= render "shared/sidebar" %> -
- -
- -
- -
-
- <%= render "shared/flash" %> - <%= yield %> +
+ <%= render "shared/sidebar" %> +
+ +
+
-
+ +
+
+ <%= render "shared/flash" %> + <%= yield %> +
+
+
<% else %> diff --git a/app/views/shared/_sidebar.html.erb b/app/views/shared/_sidebar.html.erb index de1697b..92a6231 100644 --- a/app/views/shared/_sidebar.html.erb +++ b/app/views/shared/_sidebar.html.erb @@ -90,7 +90,7 @@
  • - <%= link_to signout_path, data: { turbo_method: :delete }, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-red-600 hover:text-red-700 hover:bg-red-50" do %> + <%= link_to signout_path, data: { turbo_method: :delete, action: "click->mobile-sidebar#closeSidebar" }, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-red-600 hover:text-red-700 hover:bg-red-50" do %> @@ -105,7 +105,10 @@ -