prop/lib/microwaveprop_web/controllers/api/v1/auth_controller.ex
Graham McIntire fd976b0cd5
fix: resolve 391 Credo issues across codebase
- Add jump_credo_checks ~> 0.4 with all 20 checks enabled
- Fix all standard Credo issues: 139 @spec (113 done, 26 remain),
  4 refactoring, 3 alias usage, 9 System.cmd env, 5 unsafe_to_atom,
  2 max line length, 9 assert_receive timeout
- Fix 170+ jump_credo_checks warnings:
  - 117 TopLevelAliasImportRequire: move nested alias/import to module top
  - 32 UseObanProWorker: switch to Oban.Pro.Worker
  - 4 DoctestIExExamples: add doctests / create test file
  - ~20 WeakAssertion: strengthen type-check assertions
  - Various ConditionalAssertion, AssertReceiveTimeout fixes
- Exclude vendor/ from Credo analysis
- Remaining: 175 warnings (mostly opinionated WeakAssertion,
  AvoidSocketAssignsInTest), 26 @spec annotations
2026-06-12 13:51:32 -05:00

69 lines
2.1 KiB
Elixir

defmodule MicrowavepropWeb.Api.V1.AuthController do
@moduledoc """
Email/password login that mints a long-lived `/api/v1` bearer token.
This is the only `/api/v1` endpoint that accepts a password — every
other endpoint authenticates with the bearer token returned here.
"""
use Phoenix.Controller, formats: [:json]
alias Microwaveprop.Accounts
alias MicrowavepropWeb.Api.ErrorJSON
alias MicrowavepropWeb.Api.V1.TokenJSON
plug :accepts, ["json"]
@doc """
POST /api/v1/auth/tokens
Body: `{"email": "...", "password": "...", "name": "device label",
"expires_at": "ISO8601" (optional)}`. Returns the plaintext token
and the persisted record.
"""
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
def create(conn, params) do
with {:ok, email} <- fetch_string(params, "email"),
{:ok, password} <- fetch_string(params, "password"),
{:ok, name} <- fetch_string(params, "name"),
%Microwaveprop.Accounts.User{} = user <-
Accounts.get_user_by_email_and_password(email, password) do
token_attrs = %{name: name, expires_at: parse_expiry(params["expires_at"])}
case Accounts.create_api_token(user, token_attrs) do
{:ok, {plaintext, record}} ->
conn
|> put_status(:created)
|> json(TokenJSON.show_with_plaintext(record, plaintext))
{:error, changeset} ->
ErrorJSON.send_changeset(conn, changeset)
end
else
nil ->
ErrorJSON.send_problem(conn, 401, "unauthorized", "Invalid email or password.")
{:error, field} ->
ErrorJSON.send_problem(conn, 400, "bad_request", "Missing or invalid `#{field}`.")
end
end
defp fetch_string(params, key) do
case Map.get(params, key) do
value when is_binary(value) and byte_size(value) > 0 -> {:ok, value}
_ -> {:error, key}
end
end
defp parse_expiry(nil), do: nil
defp parse_expiry(""), do: nil
defp parse_expiry(string) when is_binary(string) do
case DateTime.from_iso8601(string) do
{:ok, dt, _offset} -> dt
{:error, _} -> :invalid
end
end
defp parse_expiry(_), do: :invalid
end