towerops/lib/towerops_web/plugs/graphql_auth.ex
graham c5481c2cd3 add mobile token auth to GraphQL endpoint (#48)
Extend the GraphQL API and socket to accept mobile session tokens
alongside existing API tokens. Add OrganizationScope middleware
that extracts organization_id from query args for mobile users.
Add my_organizations query for mobile app org listing.

Reviewed-on: graham/towerops-web#48
2026-03-16 15:23:07 -05:00

75 lines
2 KiB
Elixir

defmodule ToweropsWeb.Plugs.GraphQLAuth do
@moduledoc """
Plug for authenticating GraphQL requests using either API tokens or mobile session tokens.
Accepts two token types via Authorization: Bearer <token>:
- API tokens (prefixed with "towerops_") → sets current_organization_id + current_user
- Mobile session tokens → sets current_user only (no org_id)
Returns 401 Unauthorized if neither token type is valid.
"""
import Phoenix.Controller, only: [json: 2]
import Plug.Conn
alias Towerops.Accounts
alias Towerops.ApiTokens
alias Towerops.MobileSessions
def init(opts), do: opts
def call(conn, _opts) do
case get_req_header(conn, "authorization") do
["Bearer " <> token] ->
authenticate(conn, token)
_ ->
unauthorized(conn, "Missing or invalid Authorization header")
end
end
defp authenticate(conn, "towerops_" <> _ = token) do
case ApiTokens.verify_token(token) do
{:ok, organization_id, user} ->
conn
|> assign(:current_organization_id, organization_id)
|> assign(:current_user, user)
{:error, :invalid_token} ->
unauthorized(conn, "Invalid or expired API token")
end
end
defp authenticate(conn, token) do
with {:ok, session} <- validate_mobile_session(token),
{:ok, user} <- get_user(session.user_id) do
_ = Task.start(fn -> MobileSessions.touch_session(session) end)
assign(conn, :current_user, user)
else
{:error, _reason} ->
unauthorized(conn, "Invalid or expired authentication token")
end
end
defp validate_mobile_session(token) do
case MobileSessions.get_session_by_token(token) do
nil -> {:error, :invalid_token}
session -> {:ok, session}
end
end
defp get_user(user_id) do
case Accounts.get_user(user_id) do
nil -> {:error, :user_not_found}
user -> {:ok, user}
end
end
defp unauthorized(conn, message) do
conn
|> put_status(:unauthorized)
|> json(%{error: message})
|> halt()
end
end