defmodule ArcadiaCloud.Guardian do @moduledoc """ Verify-only Guardian implementation. arcadia-cloud never issues tokens — that is arcadia-app's job. We only decode and verify tokens minted by `Arcadia.Guardian`, then expose the claims as a lightweight identity struct. Token contract (set by arcadia-app): sub => ":" tenant_id => UUID string tenant_slug => string email => string roles => [string] """ use Guardian, otp_app: :arcadia_cloud def subject_for_token(%{"sub" => sub}, _claims) when is_binary(sub), do: {:ok, sub} def subject_for_token(_resource, _claims), do: {:error, :not_token_issuer} def resource_from_claims(claims) when is_map(claims) do {user_id, tenant_id_from_sub} = parse_subject(claims["sub"]) {:ok, %{ user_id: user_id, tenant_id: claims["tenant_id"] || tenant_id_from_sub, tenant_slug: claims["tenant_slug"], email: claims["email"], roles: claims["roles"] || [] }} end def resource_from_claims(_), do: {:error, :invalid_claims} @doc """ Dev/test helper: mint a JWT using the local secret. Production tokens are minted by arcadia-app. """ def mint_dev_token(claims_overrides \\ %{}) do defaults = %{ "sub" => "user-dev:tenant-dev", "tenant_id" => "tenant-dev", "tenant_slug" => "dev", "email" => "dev@example.com", "roles" => ["platform-admin"] } claims = Map.merge(defaults, claims_overrides) {:ok, token, _claims} = encode_and_sign(claims, claims, token_type: :access) {:ok, token} end defp parse_subject(nil), do: {nil, nil} defp parse_subject(subject) when is_binary(subject) do case String.split(subject, ":", parts: 2) do [user_id, tenant_id] -> {user_id, tenant_id} [user_id] -> {user_id, nil} end end end