Discourse-ip-alert: Need help with created plugin

AI-generated summary

The Discourse-ip-alert plugin is designed to send email alerts to administrators when a user logs in from a suspicious IP address. Despite the plugin code appearing correct, a generic “Oops – The software powering this discussion forum encountered an unexpected problem” error occurs upon login, with no clear indications in the error logs or stack trace linking it to the plugin. OrkoGrayskull has reviewed and revised the plugin multiple times, verifying site settings and hook registration, but the 500 Internal Server Error persists. The plugin uses the client_ip field from the latest authentication token to extract the IP address and checks it against a blocklist defined in the site settings (ip_alert_suspicious_ips). If the IP matches any blocked pattern, it sends a warning email to all admin users. The plugin code, including the extract_ip and ip_blocked? methods, appears to be implemented properly, but assistance is needed to resolve the issue.

This plugin is intended to send an email alert to all administrators whenever a user logs in from a suspicious IP address. It does so by extracting the IP from the latest authentication token (using the client_ip field), checking this IP against a blocklist defined in the site settings (ip_alert_suspicious_ips), and if the IP matches any blocked pattern, it sends a warning email to all admin users.

Observed Error:

Despite the plugin code seeming correct and following standard Discourse plugin patterns, I still encounter the generic “Oops – The software powering this discussion forum encountered an unexpected problem” error upon login. The error logs do not show clear indications from our [DiscourseIpAlert] log messages, and the stack trace does not pinpoint an issue directly within our plugin code.

Current Status:

I have reviewed and revised the plugin multiple times, including verifying site settings and ensuring that the hook (on(:user_logged_in)) is correctly registered. The IP extraction and email sending logic also appear to be implemented properly, yet the 500 Internal Server Error persists without a clear stack trace linking it to our plugin.

Any help?

plugin.rb:

# frozen_string_literal: true

# name: discourse-ip-alert
# about: Sends an email alert when a user logs in from a suspicious IP address.
# version: 0.7
# authors: OrkoGrayskull (revised)
# required_version: 3.5.0

enabled_site_setting :ip_alert_enabled
enabled_site_setting :ip_alert_suspicious_ips

module ::DiscourseIpAlert
  PLUGIN_NAME = "discourse-ip-alert"

  require 'ipaddr'
  require 'mail'

  # Extracts the IP address from the latest authentication token
  def self.extract_ip(user)
    token = UserAuthToken.where(user_id: user.id).order(created_at: :desc).first
    ip = token&.attributes["client_ip"]
    Rails.logger.info("[DiscourseIpAlert] extract_ip: Extracted IP from UserAuthToken: #{ip.inspect}")
    ip
  rescue => e
    Rails.logger.error("[DiscourseIpAlert] extract_ip error: #{e.message}")
    nil
  end

  # Checks if the given IP address is present in the blocklist
  def self.ip_blocked?(ip_address)
    return false if ip_address.blank?
    blocked_ips = SiteSetting.ip_alert_suspicious_ips.split(",").map(&:strip)
    Rails.logger.info("[DiscourseIpAlert] ip_blocked?: Blocked IPs: #{blocked_ips.inspect}")
    blocked_ips.any? do |blocked|
      if blocked.include?('*')
        regex = Regexp.new("^" + Regexp.escape(blocked).gsub('\\*', '\\d{1,3}') + "$")
        Rails.logger.info("[DiscourseIpAlert] ip_blocked?: Comparing #{ip_address} with wildcard regex #{regex.inspect}")
        ip_address.match?(regex)
      else
        begin
          result = IPAddr.new(blocked).include?(ip_address)
          Rails.logger.info("[DiscourseIpAlert] ip_blocked?: Comparing #{ip_address} with #{blocked}: #{result}")
          result
        rescue IPAddr::InvalidAddressError => e
          Rails.logger.error("[DiscourseIpAlert] ip_blocked?: Invalid IP in blocklist: #{blocked} (#{e.message})")
          false
        end
      end
    end
  rescue => e
    Rails.logger.error("[DiscourseIpAlert] ip_blocked? error: #{e.message}")
    false
  end

  # Sends a warning email to all admins
  def self.send_warning_email(user, ip_address)
    admin_emails = User.where(admin: true).pluck(:email)
    return if admin_emails.empty?
    subject = "⚠️ Warning: Login from a Suspicious IP!"
    body = <<~BODY
      Attention!

      The user #{user.username} (#{user.email}) has logged in from the IP address #{ip_address},
      which has been flagged as suspicious.

      Please review this account.
    BODY
    admin_emails.each do |email|
      begin
        mail = Mail.new do
          from    ENV['DISCOURSE_NOTIFICATION_EMAIL'] || 'no-reply@example.org'
          to      email
          subject subject
          body    body
        end
        Rails.logger.info("[DiscourseIpAlert] Sending warning email to #{email}")
        mail.deliver!
      rescue => e
        Rails.logger.error("[DiscourseIpAlert] Error sending email to #{email}: #{e.message}")
      end
    end
  rescue => e
    Rails.logger.error("[DiscourseIpAlert] send_warning_email error: #{e.message}")
  end

  # Processes the user login: extracts the IP, checks the blocklist, and sends a warning email if necessary
  def self.process_user_login(user)
    Rails.logger.info("[DiscourseIpAlert] process_user_login for user: #{user.username}")
    ip_address = extract_ip(user)
    unless ip_address
      Rails.logger.warn("[DiscourseIpAlert] No IP address extracted – skipping further processing.")
      return
    end
    if ip_blocked?(ip_address)
      Rails.logger.warn("[DiscourseIpAlert] Suspicious IP detected: #{ip_address}")
      send_warning_email(user, ip_address)
    else
      Rails.logger.info("[DiscourseIpAlert] IP #{ip_address} is considered safe.")
    end
  rescue => e
    Rails.logger.error("[DiscourseIpAlert] process_user_login error: #{e.message}")
  end
end

after_initialize do
  Rails.logger.info("[DiscourseIpAlert] after_initialize: Plugin is being integrated into the login process.")

  # Register the hook that is called on every user login
  on(:user_logged_in) do |user|
    begin
      Rails.logger.info("[DiscourseIpAlert] on(:user_logged_in) hook called for user: #{user.username}")
      ::DiscourseIpAlert.process_user_login(user)
    rescue => e
      Rails.logger.error("[DiscourseIpAlert] Error in on(:user_logged_in) hook: #{e.message}")
    end
  end
end

config/settings.yml:

ip_alert_enabled:
  default: false
  client: true
  type: bool
  description: "Enable the IP alert"

ip_alert_suspicious_ips:
  default: "192.168.1.1,203.0.113.0/24"
  client: true
  type: string
  description: "List of suspicious IP addresses (comma-separated)"