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

157 lines
4 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
require Logger
@doc """
Import GeoIP database batch from processed CSV data.
Requires superuser API token.
## Request
POST /admin/api/geoip/import
Content-Type: application/json
{
"batch_type": "locations", // or "blocks"
"data": [...], // Array of location or block records
"truncate": true // Optional: truncate table before inserting (first batch only)
}
## Response
200 OK
{
"status": "ok",
"inserted": 5000
}
"""
def import_database(conn, %{"batch_type" => batch_type, "data" => data} = params) do
with :ok <- require_superuser(conn) do
truncate = Map.get(params, "truncate", false)
Logger.info(
"GeoIP import batch requested by #{conn.assigns[:current_user].email}: #{batch_type}, #{length(data)} records, truncate=#{truncate}"
)
case import_batch(batch_type, data, truncate) do
{:ok, inserted_count} ->
conn
|> put_status(:ok)
|> json(%{
status: "ok",
inserted: inserted_count
})
{:error, reason} ->
Logger.error("GeoIP batch import failed: #{inspect(reason)}")
conn
|> put_status(:internal_server_error)
|> json(%{error: "Import failed: #{inspect(reason)}"})
end
end
end
def import_database(conn, _params) do
conn
|> put_status(:bad_request)
|> json(%{
error: "Missing required parameters: batch_type and data"
})
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 import_batch("locations", data, truncate) do
alias Towerops.GeoIP.Location
alias Towerops.Repo
if truncate do
Repo.delete_all(Location)
end
now = DateTime.truncate(DateTime.utc_now(), :second)
entries =
Enum.map(data, fn location ->
%{
geoname_id: location["geoname_id"],
country_code: location["country_code"],
country_name: location["country_name"],
city_name: location["city_name"],
subdivision_1_name: location["subdivision_1_name"],
subdivision_2_name: location["subdivision_2_name"],
latitude: location["latitude"],
longitude: location["longitude"],
inserted_at: now,
updated_at: now
}
end)
{inserted, _} = Repo.insert_all(Location, entries)
{:ok, inserted}
rescue
e ->
{:error, Exception.message(e)}
end
defp import_batch("blocks", data, truncate) do
alias Towerops.GeoIP.Block
alias Towerops.Repo
if truncate do
Repo.delete_all(Block)
end
now = DateTime.truncate(DateTime.utc_now(), :second)
entries =
Enum.map(data, fn block ->
%{
id: Ecto.UUID.generate(),
network: block["network"],
start_ip_int: block["start_ip_int"],
end_ip_int: block["end_ip_int"],
geoname_id: block["geoname_id"],
registered_country_geoname_id: block["registered_country_geoname_id"],
inserted_at: now,
updated_at: now
}
end)
{inserted, _} = Repo.insert_all(Block, entries)
{:ok, inserted}
rescue
e ->
{:error, Exception.message(e)}
end
defp import_batch(_type, _data, _truncate) do
{:error, "Invalid batch_type. Must be 'locations' or 'blocks'"}
end
end