towerops/lib/towerops_web/controllers/api/v1/geoip_controller.ex

137 lines
3.6 KiB
Elixir

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