towerops/lib/towerops_web/controllers/api/v1/profiles_controller.ex
Graham McIntire 56093bb493
refactor: use API token auth for profile imports instead of session cookies
Changes profile import endpoint to use standard API token authentication:

API Token Changes:
- Add user_id to api_tokens table (tracks who created the token)
- Update ApiTokens.verify_token/1 to return user along with org_id
- Update ApiAuth plug to assign current_user from token

Profile Import Changes:
- Move endpoint from /api/v1/admin/profiles/import to /api/v1/profiles/import
- Check user.is_superuser in controller instead of using RequireSuperuser plug
- Use api_v1 pipeline (Bearer token auth) instead of browser session
- Update documentation to show API token usage

Security:
- Only API tokens created by superusers can import profiles
- Returns 403 Forbidden if token user is not a superuser
- Logs import attempts with user email for audit trail

This provides a consistent API experience using Bearer tokens
instead of requiring browser session cookies.
2026-01-18 09:30:21 -06:00

94 lines
2.6 KiB
Elixir

defmodule ToweropsWeb.Api.V1.ProfilesController do
@moduledoc """
API controller for managing device SNMP profiles.
All endpoints require API token authentication.
"""
use ToweropsWeb, :controller
require Logger
@doc """
POST /api/v1/profiles/import
Imports device profiles from JSON data exported via `mix export_profiles`.
**Requires superuser API token** - Only API tokens created by superuser accounts can import profiles.
Request body:
[
{
"os": "epmp",
"detection": { ... }, # YAML detection data
"discovery": { ... } # YAML discovery data (optional)
},
...
]
Response:
{
"status": "queued",
"job_id": "uuid",
"profile_count": 15,
"message": "Import job queued successfully..."
}
Example:
curl -X POST https://towerops.net/api/v1/profiles/import \\
-H "Authorization: Bearer YOUR_SUPERUSER_TOKEN" \\
-H "Content-Type: application/json" \\
-d @profiles.json
"""
def import_bulk(conn, %{"_json" => profiles}) when is_list(profiles) do
# Check if user is a superuser
user = conn.assigns[:current_user]
if !user || !user.is_superuser do
Logger.warning("Profile 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 profiles."})
else
Logger.info("Queueing import for #{length(profiles)} device profiles via API (user: #{user.email})")
do_import_bulk(conn, profiles)
end
end
def import_bulk(conn, _params) do
conn
|> put_status(:bad_request)
|> json(%{error: "Invalid request format. Expected array of profile objects."})
end
# Private functions
defp do_import_bulk(conn, profiles) do
# Generate unique job ID for tracking
job_id = Ecto.UUID.generate()
# Enqueue background job
case Exq.enqueue(Exq, "maintenance", Towerops.Workers.ProfileImportWorker, [
profiles,
job_id
]) do
{:ok, _job_jid} ->
Logger.info("Profile import job #{job_id} queued successfully")
json(conn, %{
status: "queued",
job_id: job_id,
profile_count: length(profiles),
message: "Import job queued successfully. #{length(profiles)} profiles will be imported in the background."
})
{:error, reason} ->
Logger.error("Failed to queue profile import: #{inspect(reason)}")
conn
|> put_status(:internal_server_error)
|> json(%{error: "Failed to queue import job", reason: inspect(reason)})
end
end
end