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 : - 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.Supervisor.start_child(Towerops.TaskSupervisor, 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 -> # Defense in depth: the query already filters expired rows, but # explicitly verifying here means a refactor that drops the filter # can't quietly re-enable revoked sessions. if session_expired?(session) do {:error, :invalid_token} else {:ok, session} end end end defp session_expired?(%{expires_at: nil}), do: true defp session_expired?(%{expires_at: expires_at}) do DateTime.compare(expires_at, DateTime.utc_now()) != :gt 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