towerops/lib/towerops_web/live/admin/security_live/index.ex
Graham McIntire 91e3181bbc dialyzer: fix all unmatched_return warnings (154 → 0)
Prefix fire-and-forget calls (PubSub.broadcast, Oban enqueue, Logger,
update_*, etc.) with `_ =` so dialyzer knows the return value is
intentionally discarded. Wrap a few if/case expressions whose
branches produce mixed types the same way.

No behavior changes — only explicit acknowledgement of discarded
returns.

Warnings: 242 → 88.
2026-04-21 10:03:55 -05:00

130 lines
3.7 KiB
Elixir

defmodule ToweropsWeb.Admin.SecurityLive.Index do
@moduledoc """
Admin page for managing IP-based brute force protection.
Provides two main functions:
1. Allowlist management - Add/remove IPs and CIDR ranges that bypass protection
2. Denylist management - View and manually unblock banned IPs
"""
use ToweropsWeb, :live_view
alias Towerops.Security.BruteForce
@impl true
def mount(_params, _session, socket) do
_ =
if connected?(socket) do
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "security:whitelist")
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "security:blocks")
end
{:ok,
socket
|> assign(:page_title, t("Security - IP Access Control"))
|> assign(:current_tab, "whitelist")}
end
@impl true
def handle_params(params, _url, socket) do
tab = Map.get(params, "tab", "whitelist")
filter = Map.get(params, "filter", "all")
{:noreply,
socket
|> assign(:current_tab, tab)
|> assign(:filter, filter)
|> load_data()}
end
@impl true
def handle_event("change_tab", %{"tab" => tab}, socket) do
{:noreply, push_patch(socket, to: ~p"/admin/security?tab=#{tab}")}
end
def handle_event("change_filter", %{"filter" => filter}, socket) do
{:noreply, push_patch(socket, to: ~p"/admin/security?tab=#{socket.assigns.current_tab}&filter=#{filter}")}
end
def handle_event("show_whitelist_form", _params, socket) do
{:noreply,
socket
|> assign(:show_whitelist_form, true)
|> assign(:whitelist_form, to_form(%{}))}
end
def handle_event("hide_whitelist_form", _params, socket) do
{:noreply, assign(socket, :show_whitelist_form, false)}
end
def handle_event("add_whitelist", %{"whitelist" => params}, socket) do
case BruteForce.add_to_whitelist(
params["ip_or_cidr"],
params["description"],
socket.assigns.current_user
) do
{:ok, _entry} ->
{:noreply,
socket
|> put_flash(:info, t("IP/CIDR added to allowlist"))
|> assign(:show_whitelist_form, false)
|> load_data()}
{:error, changeset} ->
{:noreply,
socket
|> assign(:whitelist_form, to_form(Map.put(changeset, :action, :validate)))
|> put_flash(:error, t("Failed to add to allowlist"))}
end
end
def handle_event("remove_whitelist", %{"id" => id}, socket) do
case BruteForce.remove_from_whitelist(id) do
:ok ->
{:noreply,
socket
|> put_flash(:info, t("Removed from allowlist"))
|> load_data()}
{:error, _} ->
{:noreply, put_flash(socket, :error, t("Failed to remove from allowlist"))}
end
end
def handle_event("unblock_ip", %{"ip" => ip_address}, socket) do
case BruteForce.manually_unblock(ip_address) do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, t("IP %{ip_address} unblocked", ip_address: ip_address))
|> load_data()}
{:error, _} ->
{:noreply, put_flash(socket, :error, t("Failed to unblock IP"))}
end
end
@impl true
def handle_info(:whitelist_updated, socket) do
{:noreply, load_data(socket)}
end
def handle_info(:blocks_updated, socket) do
{:noreply, load_data(socket)}
end
defp load_data(socket) do
whitelist = BruteForce.list_whitelist()
blocked_ips =
case socket.assigns[:filter] || "all" do
"permanent" -> BruteForce.list_blocked_ips(:permanent)
"temporary" -> BruteForce.list_blocked_ips(:temporary)
_ -> BruteForce.list_blocked_ips(:all)
end
socket
|> assign(:whitelist, whitelist)
|> assign(:blocked_ips, blocked_ips)
|> assign(:show_whitelist_form, Map.get(socket.assigns, :show_whitelist_form, false))
end
end