defmodule ToweropsWeb.Api.V1.GeoipController do @moduledoc """ API controller for managing GeoIP database imports. Allows superusers to trigger GeoIP database imports from MaxMind CSV files. All endpoints require superuser API token authentication. """ use ToweropsWeb, :controller alias Mix.Tasks.Geoip.Import require Logger @doc """ Import GeoIP database from MaxMind GeoLite2-City CSV files. Requires superuser API token. ## Request POST /api/v1/geoip/import Content-Type: application/json { "directory": "/path/to/GeoLite2-City-CSV_20260127" } ## Response 202 Accepted { "status": "ok", "message": "GeoIP import started in background" } Or: 200 OK { "status": "ok", "message": "GeoIP database imported successfully", "locations_count": 130000, "blocks_count": 3500000 } """ def import_database(conn, %{"directory" => directory}) do with :ok <- require_superuser(conn) do Logger.info("GeoIP import requested by #{conn.assigns[:current_user].email} for directory: #{directory}") # Expand the path to handle ~ and relative paths expanded_directory = Path.expand(directory) # Verify directory exists if File.dir?(expanded_directory) do # Run import synchronously (it's fast enough with batching) case run_import(expanded_directory) do {:ok, locations_count, blocks_count} -> Logger.info("GeoIP import completed: #{locations_count} locations, #{blocks_count} blocks") conn |> put_status(:ok) |> json(%{ status: "ok", message: "GeoIP database imported successfully", locations_count: locations_count, blocks_count: blocks_count }) {:error, reason} -> Logger.error("GeoIP import failed: #{inspect(reason)}") conn |> put_status(:internal_server_error) |> json(%{error: "Import failed: #{inspect(reason)}"}) end else conn |> put_status(:bad_request) |> json(%{error: "Directory does not exist: #{directory}"}) end end end def import_database(conn, _params) do conn |> put_status(:bad_request) |> json(%{error: "Missing required parameter: directory"}) end defp require_superuser(conn) do user = conn.assigns[:current_user] if !user || !user.is_superuser do Logger.warning("GeoIP import attempted by non-superuser: #{inspect(user && user.email)}") conn |> put_status(:forbidden) |> json(%{ error: "Superuser access required. Only API tokens created by superusers can import GeoIP data." }) |> halt() else :ok end end defp run_import(directory) do alias Towerops.GeoIP.Block alias Towerops.GeoIP.Location alias Towerops.Repo blocks_file = Path.join(directory, "GeoLite2-City-Blocks-IPv4.csv") locations_file = Path.join(directory, "GeoLite2-City-Locations-en.csv") # Verify files exist if File.exists?(blocks_file) && File.exists?(locations_file) do # Truncate existing data Repo.delete_all(Block) Repo.delete_all(Location) # Load locations {locations_count, valid_geoname_ids} = Import.load_locations(locations_file) # Load blocks blocks_count = Import.load_blocks(blocks_file, valid_geoname_ids) {:ok, locations_count, blocks_count} else {:error, "Required CSV files not found in directory"} end rescue e -> {:error, Exception.message(e)} end end