towerops/lib/towerops_web/plugs/api_auth.ex
Graham McIntire c4760ca0dc
Add API v1 endpoints with organization-scoped API tokens
Created API token system for programmatic access:
- API tokens table with organization scoping
- ApiTokens context for token management
- ApiAuth plug for Bearer token authentication
- Tokens shown once on creation, then hashed for security

Implemented RESTful API v1 endpoints:
- SitesController: CRUD operations for sites
- DevicesController: CRUD operations for devices
- All operations scoped to authenticated organization
- Proper authorization checks via site ownership

Technical details:
- Tokens prefixed with "towerops_" for easy identification
- SHA-256 hashing for token storage
- Last used timestamp tracking (async update)
- Optional token expiration support
- Standard JSON error responses (40x status codes)

Routes:
- /api/v1/sites (GET, POST, PATCH, DELETE)
- /api/v1/devices (GET, POST, PATCH, DELETE)

Authentication:
- Authorization: Bearer towerops_xxxxx header required
- Returns 401 for invalid/expired tokens
- Returns 403 for unauthorized resource access
2026-01-17 16:53:31 -06:00

52 lines
1.3 KiB
Elixir

defmodule ToweropsWeb.Plugs.ApiAuth do
@moduledoc """
Plug for authenticating API requests using Bearer tokens.
Expects the Authorization header to contain a Bearer token:
Authorization: Bearer towerops_xxxxx
Sets the following assigns on successful authentication:
- :current_organization_id - The organization ID the token is scoped to
- :api_authenticated - true
Returns 401 Unauthorized if:
- No Authorization header is present
- Token is invalid or expired
- Token format is incorrect
"""
import Phoenix.Controller, only: [json: 2]
import Plug.Conn
alias Towerops.ApiTokens
def init(opts), do: opts
def call(conn, _opts) do
case get_req_header(conn, "authorization") do
["Bearer " <> token] ->
authenticate_with_token(conn, token)
_ ->
conn
|> put_status(:unauthorized)
|> json(%{error: "Missing or invalid Authorization header"})
|> halt()
end
end
defp authenticate_with_token(conn, token) do
case ApiTokens.verify_token(token) do
{:ok, organization_id} ->
conn
|> assign(:current_organization_id, organization_id)
|> assign(:api_authenticated, true)
{:error, :invalid_token} ->
conn
|> put_status(:unauthorized)
|> json(%{error: "Invalid or expired API token"})
|> halt()
end
end
end