# frozen_string_literal: true class Project < ApplicationRecord has_many :events, dependent: :destroy validates :name, presence: true validates :slug, presence: true, uniqueness: true validates :public_key, presence: true, uniqueness: true scope :by_slug, ->(slug) { where(slug: slug) } scope :by_public_key, ->(key) { where(public_key: key) } scope :enabled, -> { where(enabled: true) } before_validation :generate_slug, if: :name? before_validation :generate_public_key, if: -> { public_key.blank? } before_validation :set_default_settings, if: -> { settings.blank? } def broadcast_events_refresh # Broadcast to the events stream for this project broadcast_refresh_to(self, "events") end def broadcast_rules_refresh # Broadcast to the rules stream for this project (for future rule management UI) broadcast_refresh_to(self, "rules") end def self.find_by_dsn(dsn) # Parse DSN: https://public_key@host/project_id return nil unless dsn.present? # Extract public_key from DSN match = dsn.match(/https?:\/\/([^@]+)@/) return nil unless match public_key = match[1] find_by(public_key: public_key) end def self.find_by_project_id(project_id) # Try slug first (nicer URLs), then fall back to ID find_by(slug: project_id.to_s) || find_by(id: project_id.to_i) end def dsn host = Current.baffle_host || "localhost:3000" protocol = host.include?("localhost") ? "http" : "https" "#{protocol}://#{public_key}@#{host}/#{slug}" end def internal_dsn return nil unless Current.baffle_internal_host.present? host = Current.baffle_internal_host protocol = "http" # Internal connections use HTTP "#{protocol}://#{public_key}@#{host}/#{slug}" end # WAF Analytics Methods def recent_events(limit: 100) events.recent.limit(limit) end def recent_blocked_events(limit: 100) events.blocked.recent.limit(limit) end def recent_rate_limited_events(limit: 100) events.rate_limited.recent.limit(limit) end def top_blocked_ips(limit: 10, time_range: 1.hour.ago) events.blocked .where(timestamp: time_range) .group(:ip_address) .select('ip_address, COUNT(*) as count') .order('count DESC') .limit(limit) end def event_count(time_range = nil) if time_range events.where(timestamp: time_range).count else events.count end end def blocked_count(time_range = nil) if time_range events.blocked.where(timestamp: time_range).count else events.blocked.count end end def allowed_count(time_range = nil) if time_range events.allowed.where(timestamp: time_range).count else events.allowed.count end end # Helper method to parse settings safely def parsed_settings if settings.is_a?(String) JSON.parse(settings || '{}') else settings || {} end rescue JSON::ParserError {} end # WAF Configuration Methods def rate_limit_enabled? parsed_settings.dig('rate_limiting', 'enabled') != false end def rate_limit_threshold parsed_settings.dig('rate_limiting', 'threshold') || 100 end def custom_rules_enabled? parsed_settings.dig('custom_rules', 'enabled') == true end def block_by_country_enabled? parsed_settings.dig('geo_blocking', 'enabled') == true end def blocked_countries parsed_settings.dig('geo_blocking', 'blocked_countries') || [] end def block_datacenters_enabled? parsed_settings.dig('datacenter_blocking', 'enabled') == true end # WAF Rule Management def add_ip_rule(ip_address, action, expires_at: nil, reason: nil) # This will integrate with the IP rules storage system # For now, store in settings as a temporary solution current_settings = parsed_settings ip_rules = current_settings['ip_rules'] || {} ip_rules[ip_address] = { action: action, expires_at: expires_at&.iso8601, reason: reason, created_at: Time.current.iso8601 } update(settings: current_settings.merge('ip_rules' => ip_rules)) end def remove_ip_rule(ip_address) current_settings = parsed_settings ip_rules = current_settings['ip_rules'] || {} ip_rules.delete(ip_address) update(settings: current_settings.merge('ip_rules' => ip_rules)) end def blocked_ips ip_rules = parsed_settings['ip_rules'] || {} ip_rules.select { |_ip, rule| rule['action'] == 'block' }.keys end def waf_status return 'disabled' unless enabled? return 'active' if events.where(timestamp: 1.hour.ago..).exists? 'idle' end private def generate_slug self.slug = name&.parameterize&.downcase end def generate_public_key # Generate a random 32-character hex string for WAF authentication self.public_key = SecureRandom.hex(16) end def set_default_settings self.settings = { 'rate_limiting' => { 'enabled' => true, 'threshold' => 100, # requests per minute 'window' => 60 # seconds }, 'geo_blocking' => { 'enabled' => false, 'blocked_countries' => [] }, 'datacenter_blocking' => { 'enabled' => false, 'allow_known_datacenters' => true }, 'custom_rules' => { 'enabled' => false, 'rules' => [] }, 'ip_rules' => {}, 'challenge' => { 'enabled' => true, 'provider' => 'recaptcha' } } end end