Ecto.Association.NotLoaded

ecto
phoenix

#1

In Phx 1.3 this code works, but in Phx 1.4 doesn’t want.

  def index(conn, _params) do
    current_user = Guardian.Plug.current_resource(conn)

    owned_board = current_user
      |> assoc(:owned_board)
      |> board_preload_all
      |> Repo.all

    invited_board = current_user
      |> assoc(:board)
      |> not_owned_by(current_user.id)
      |> board_preload_all
      |> Repo.all

    render(conn, "index.json", owned_board: owned_board, invited_board: invited_board)
  end

Errors are obtained.

[error] #PID<0.501.0> running ImconWeb.Endpoint (connection #PID<0.500.0>, stream id 1) terminated
Server: caix.ru:4001 (http)
Request: POST /api/v1/sessions
** (exit) an exception was raised:
    ** (ArgumentError) argument error
        :erlang.apply(%Imcon.Auth.User{__meta__: #Ecto.Schema.Metadata<:loaded, "user">, board: #Ecto.Association.NotLoaded<association :board is not loaded>, email: "john@phoenix-trello.com", encrypted_password: "$2b$12$nn1jeBQwQms.Id2JOeb6..mi8UQQka8dyWNkh0WqmAm/hN0AQX9zW", first_name: "John", id: 1, inserted_at: ~N[2019-01-02 08:36:35.417355], is_admin: false, last_name: "Doe", owned_board: #Ecto.Association.NotLoaded<association :owned_board is not loaded>, password: nil, updated_at: ~N[2019-01-02 08:36:35.426004], user_board: #Ecto.Association.NotLoaded<association :user_board is not loaded>}, :config, [])
        (guardian) lib/guardian.ex:776: Guardian.token_module/1
        (guardian) lib/guardian.ex:576: Guardian.encode_and_sign/4
        (imcon) lib/imcon_web/controllers/api/v1/session_controller.ex:9: ImconWeb.SessionController.create/2

The configuration is such here.

[
  {:phoenix, "~> 1.4.0"},
  {:phoenix_pubsub, "~> 1.1.1"},
  {:phoenix_ecto, ">= 3.2.0 and < 3.5.0"},
  {:postgrex, ">= 0.0.0"},
  {:poison, ">= 0.0.0"},
  {:phoenix_html, "~> 2.12.0"},
  {:phoenix_live_reload, "~> 1.2.0", only: :dev},
  {:gettext, "~> 0.16.1"},
  {:cowboy, "~> 2.6.0"},
  {:plug_cowboy, "~> 2.0.0"},
  {:comeonin, "~> 4.1.1"},
  {:bcrypt_elixir, "~> 1.0.9"},
  {:guardian, "~> 1.1.1"},
  {:credo, "~> 0.10.2", only: [:dev, :test]},
  {:ex_machina, "~> 2.2.2"},
  {:exactor, "~> 2.2.4"},
  {:hound, "~> 1.0.2"},
  {:mix_test_watch, "~> 0.9.0", only: :dev},
  {:poolboy, "~> 1.5.1"}
]

#2

As error message said the problem occurs in create action not in index.


#3

Which version of ecto is effectively used, please show us the appropriate line of mix.lock. If it is >= 3.0 please tell us about ecto_sql as well.


#4

“ecto”: {:hex, :ecto, “2.2.11”, “4bb8f11718b72ba97a2696f65d247a379e739a0ecabf6a13ad1face79844791c”, [:mix], [{:db_connection, “~> 1.1”, [hex: :db_connection, repo: “hexpm”, optional: true]}, {:decimal, “~> 1.2”, [hex: :decimal, repo: “hexpm”, optional: false]}, {:mariaex, “~> 0.8.0”, [hex: :mariaex, repo: “hexpm”, optional: true]}, {:poison, “~> 2.2 or ~> 3.0”, [hex: :poison, repo: “hexpm”, optional: true]}, {:poolboy, “~> 1.5”, [hex: :poolboy, repo: “hexpm”, optional: false]}, {:postgrex, “~> 0.13.0”, [hex: :postgrex, repo: “hexpm”, optional: true]}, {:sbroker, “~> 1.0”, [hex: :sbroker, repo: “hexpm”, optional: true]}], “hexpm”},
“ex_machina”: {:hex, :ex_machina, “2.2.2”, “d84217a6fb7840ff771d2561b8aa6d74a0d8968e4b10ecc0d7e9890dc8fb1c6a”, [:mix], [{:ecto, “~> 2.2 or ~> 3.0”, [hex: :ecto, repo: “hexpm”, optional: true]}, {:ecto_sql, “~> 3.0”, [hex: :ecto_sql, repo: “hexpm”, optional: true]}], “hexpm”},

ecto_sql not installed, as I understand it.


#5

[
{:phoenix, “~> 1.4.0”},
{:phoenix_pubsub, “~> 1.1.1”},
{:phoenix_ecto, “~> 4.0.0”},
{:ecto, “>= 3.0.6”},
{:ecto_sql, “~> 3.0.4”},
{:postgrex, “>= 0.0.0”},
{:poison, “>= 0.0.0”},
{:phoenix_html, “~> 2.12.0”},
{:phoenix_live_reload, “~> 1.2.0”, only: :dev},
{:gettext, “~> 0.16.1”},
{:cowboy, “~> 2.6.0”},
{:plug_cowboy, “~> 2.0.0”},
{:comeonin, “~> 4.1.1”},
{:bcrypt_elixir, “~> 1.0.9”},
{:guardian, “~> 1.1.1”},
{:credo, “~> 0.10.2”, only: [:dev, :test]},
{:ex_machina, “~> 2.2.2”},
{:exactor, “~> 2.2.4”},
{:hound, “~> 1.0.2”},
{:mix_test_watch, “~> 0.9.0”, only: :dev},
{:poolboy, “~> 1.5.1”}
]
end

“db_connection”: {:hex, :db_connection, “2.0.3”, “b4e8aa43c100e16f122ccd6798cd51c48c79fd391c39d411f42b3cd765daccb0”, [:mix], [{:connection, “~> 1.0.2”, [hex: :connection, repo: “hexpm”, optional: false]}], “hexpm”},
“decimal”: {:hex, :decimal, “1.6.0”, “bfd84d90ff966e1f5d4370bdd3943432d8f65f07d3bab48001aebd7030590dcc”, [:mix], [], “hexpm”},
“ecto”: {:hex, :ecto, “3.0.6”, “d33ab5b3f7553a41507d4b0ad5bf192d533119c4ad08f3a5d63d85aa12117dc9”, [:mix], [{:decimal, “~> 1.6”, [hex: :decimal, repo: “hexpm”, optional: false]}, {:jason, “~> 1.0”, [hex: :jason, repo: “hexpm”, optional: true]}, {:poison, “~> 2.2 or ~> 3.0”, [hex: :poison, repo: “hexpm”, optional: true]}], “hexpm”},
“ecto_sql”: {:hex, :ecto_sql, “3.0.4”, “e7a0feb0b2484b90981c56d5cd03c52122c1c31ded0b95ed213b7c5c07ae6737”, [:mix], [{:db_connection, “~> 2.0”, [hex: :db_connection, repo: “hexpm”, optional: false]}, {:ecto, “~> 3.0.6”, [hex: :ecto, repo: “hexpm”, optional: false]}, {:mariaex, “~> 0.9.1”, [hex: :mariaex, repo: “hexpm”, optional: true]}, {:postgrex, “~> 0.14.0”, [hex: :postgrex, repo: “hexpm”, optional: true]}, {:telemetry, “~> 0.3.0”, [hex: :telemetry, repo: “hexpm”, optional: false]}], “hexpm”},
“elixir_make”: {:hex, :elixir_make, “0.4.2”, “332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf”, [:mix], [], “hexpm”},
“ex_machina”: {:hex, :ex_machina, “2.2.2”, “d84217a6fb7840ff771d2561b8aa6d74a0d8968e4b10ecc0d7e9890dc8fb1c6a”, [:mix], [{:ecto, “~> 2.2 or ~> 3.0”, [hex: :ecto, repo: “hexpm”, optional: true]}, {:ecto_sql, “~> 3.0”, [hex: :ecto_sql, repo: “hexpm”, optional: true]}], “hexpm”},

Errors, in the end, remained the same.


#6

What else ideas can be how to overcome this error?


#7

** (ArgumentError) argument error
:erlang.apply(%Imcon.Auth.User{meta: #Ecto.Schema.Metadata<:loaded, “user”>, board: #Ecto.Association.NotLoaded, email: "john@phoenix-trello.com", encrypted_password: “$2b$12$nn1jeBQwQms.Id2JOeb6…mi8UQQka8dyWNkh0WqmAm/hN0AQX9zW”, first_name: “John”, id: 1, inserted_at: ~N[2019-01-02 08:36:35], is_admin: false, last_name: “Doe”, owned_board: #Ecto.Association.NotLoaded, password: nil, updated_at: ~N[2019-01-02 08:36:35], user_board: #Ecto.Association.NotLoaded}, :config, [])

Судя, по сообщению ошибки, не получается загрузить ассоциацию второго уровня. Вот схемы, я только добавил поле is_admin.

Judging by the error message, I can’t download the second level association. Here are the schemes, I just added the is_admin field.

  schema "user" do
    
    field :first_name, :string
    field :last_name, :string
    field :email, :string
    field :is_admin, :boolean, default: false
    field :encrypted_password, :string
    field :password, :string, virtual: true

    has_many :owned_board, Board
    has_many :user_board, UserBoard
    has_many :board, through: [:user_board, :board]

Maybe there is a problem somewhere?

  schema "board" do
    field :name, :string, null: false
    field :slug, :string, null: false

    belongs_to :user, User
    has_many :list, List
    has_many :card, through: [:list, :card]
    has_many :user_board, UserBoard
    has_many :member, through: [:user_board, :user]

Here I did not change anything at all yet.

  schema "user_board" do
    belongs_to :user, User
    belongs_to :board, Board

#8

Ecto doesn’t load associations by default. So this is an expected behaviour.

Error message says that you try to call :erlang.apply/3 with your user model as a first arg, but it should be a module name. That’s why ArgumentError is raised.

Again, according to your first post, error occurs inside the guardian application code after something in session_controller, line 9. So, it would be helpful to look at session_controller create action.


#9

If Erlang himself swears, then, is the error still covered in the communication modules with the database?

  defmodule ImconWeb.SessionController do
      use ImconWeb, :controller

  plug :scrub_params, "session" when action in [:create]

  def create(conn, %{"session" => session_params}) do
    case Imcon.Auth.authenticate(session_params) do
      {:ok, user} ->
        {:ok, jwt, _full_claims} = user |> Guardian.encode_and_sign(:token)

        conn
        |> put_status(:created)
        |> render("show.json", jwt: jwt, user: user)

      :error ->
        conn
        |> put_status(:unprocessable_entity)
        |> render("error.json")
    end
  end

  def delete(conn, _) do
    {:ok, claims} = Guardian.Plug.current_claims(conn)

    conn
    |> Guardian.Plug.current_token
    |> Guardian.revoke(claims)

    conn
    |> render("delete.json")
  end


  def unauthenticated(conn, _params) do
    conn
    |> put_status(:forbidden)
    |> render(ImconWeb.SessionView, "forbidden.json", error: "Not Authenticated")
  end

end

Based on various examples, I brought the authentication to a separate new context. I want to enter roles, for users, groups (teams), adding this context.

defmodule Imcon.Auth do

  import Ecto.Changeset
  import Plug.Conn
  
  alias Comeonin.Bcrypt

  alias Imcon.Repo
  alias Imcon.Auth.User

    # ... User

  def get_user(id), do: Repo.get(User, id)

  def fetch_assoc(%User{} = user, assoc \\ [:board, :user_board, :owned_board]) do
    Repo.preload(user, assoc)
  end

  @required_fields ~w(first_name last_name email password)
  @optional_fields ~w(encrypted_password)

  def create_user(%User{} = user, attrs) do
    user
    |> cast(attrs, @required_fields, @optional_fields)
    |> validate_format(:email, ~r/@/)
    |> validate_length(:password, min: 5)
    |> validate_confirmation(:password, message: "Password does not match")
    |> unique_constraint(:email, message: "Email already taken")
    |> generate_encrypted_password
  end

  def update_user(%User{} = user, attrs) do
    user
    |> cast(attrs, [:first_name, :last_name, :email], [:password])
    |> validate_required([:first_name, :email])
    |> generate_encrypted_password
    |> unique_constraint(:email)
  end

  defp generate_encrypted_password(current_changeset) do
    case current_changeset do
      %Ecto.Changeset{valid?: true, changes: %{password: password}} ->
        put_change(current_changeset, :encrypted_password, Comeonin.Bcrypt.hashpwsalt(password))
      _ ->
        current_changeset
    end
  end

  def load_current_user(conn, _) do
    conn
    |> assign(:current_user, Guardian.Plug.current_resource(conn))
    |> put_user_token(Guardian.Plug.current_resource(conn))
  end

  defp put_user_token(conn, user) do
    token = Phoenix.Token.sign(conn, "user socket", user.id)

    conn
    |> assign(:user_token, token)
  end

  def authenticate(%{"email" => email, "password" => password}) do
    user = Repo.get_by(User, email: String.downcase(email))

    case check_password(user, password) do
      true -> {:ok, user}
      _ -> :error
    end
  end

  defp check_password(user, password) do
    case user do
      nil -> Bcrypt.dummy_checkpw()
      _ -> Bcrypt.checkpw(password, user.encrypted_password)
    end
  end

end

#10

Most likely I have copied and pasted something wrong. It is necessary to open a new topic on authentication.

** (exit) an exception was raised:
** (CaseClauseError) no case clause matching: true
(imcon) lib/imcon_web/controllers/api/v1/session_controller.ex:7: ImconWeb.SessionController.create/2
(imcon) lib/imcon_web/controllers/api/v1/session_controller.ex:1: ImconWeb.SessionController.action/2
(imcon) lib/imcon_web/controllers/api/v1/session_controller.ex:1: ImconWeb.SessionController.phoenix_controller_pipeline/2
(imcon) lib/imcon_web/endpoint.ex:1: ImconWeb.Endpoint.instrument/4
(phoenix) lib/phoenix/router.ex:275: Phoenix.Router.call/1
(imcon) lib/imcon_web/endpoint.ex:1: ImconWeb.Endpoint.plug_builder_call/2
(imcon) lib/plug/debugger.ex:122: ImconWeb.Endpoint.“call (overridable 3)”/2
(imcon) lib/imcon_web/endpoint.ex:1: ImconWeb.Endpoint.call/2
(phoenix) lib/phoenix/endpoint/cowboy2_handler.ex:34: Phoenix.Endpoint.Cowboy2Handler.init/2
(cowboy) /opt/imcon/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2
(cowboy) /opt/imcon/deps/cowboy/src/cowboy_stream_h.erl:296: :cowboy_stream_h.execute/3
(cowboy) /opt/imcon/deps/cowboy/src/cowboy_stream_h.erl:274: :cowboy_stream_h.request_process/3
(stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3

defmodule ImconWeb.SessionController do
  use ImconWeb, :controller

  plug :scrub_params, "session" when action in [:create]

  def create(conn, %{"session" => %{"email" => email, "password" => password}}) do
    case Imcon.Auth.authenticate(email, password) do
      {:ok, user} ->
        {:ok, jwt, _full_claims} = user |> Guardian.encode_and_sign(:token)

        conn
        |> put_status(:created)
        |> render("show.json", jwt: jwt, user: user)

      :error ->
        conn
        |> put_status(:unprocessable_entity)
        |> render("error.json")
    end
  end

  def delete(conn, _) do
    {:ok, claims} = Guardian.Plug.current_claims(conn)

    conn
    |> Guardian.Plug.current_token
    |> Guardian.revoke(claims)

    conn
    |> render("delete.json")
  end
  
  def unauthenticated(conn, _params) do
    conn
    |> put_status(:forbidden)
    |> render(ImconWeb.SessionView, "forbidden.json", error: "Not Authenticated")
  end

end

Fragment from file auth.ex

  def authenticate(email, encrypted_password) do
    query = Ecto.Query.from(u in User, where: u.email == ^email)
    |> limit(1)
    Repo.one(query)
    |> check_password(encrypted_password)
  end

  defp check_password(user, password) do
    case user do
      nil -> Bcrypt.dummy_checkpw()
      _ -> Bcrypt.checkpw(password, user.encrypted_password)
    end
  end

#11

The issue from the first post occurs in this line:

According to guardian docs the first argument is the module implementing actual encoding in callbacks, but you pass user struct as first arg.


#12

#13
defmodule ImconWeb.Router do
  use ImconWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :auth do
    plug Imcon.Plugs
  end

  pipeline :api do
    plug :accepts, ["json", "xml"]
    plug :fetch_session
  end

  scope "/api", ImconWeb do
    pipe_through :api

    scope "/v1" do
      post "/registrations", RegistrationController, :create

      post "/sessions", SessionController, :create
      delete "/sessions", SessionController, :delete

      get "/current_user", CurrentUserController, :show

      resources "/board", BoardController, only: [:index, :create] do
      resources "/card", CardController, only: [:show]
      end
    end
  end

  scope "/", ImconWeb do
    pipe_through :browser # Use the default browser stack

    get "/*path", PageController, :index
  end
end

defmodule Imcon.Plugs do

  use Guardian.Plug.Pipeline,
    otp_app: :imcon,
    error_handler: ImconWeb.ErrorHandler,
    module: Imcon.Auth.Guard

  plug Guardian.Plug.VerifySession, claims: %{"typ" => "access"}
  plug Guardian.Plug.EnsureAuthenticated, handler: ImconWeb.SessionController
  plug Guardian.Plug.LoadResource, allow_blank: true

end

defmodule Imcon.Auth.Guard do
  use Guardian, otp_app: :imcon

  def subject_for_token(user, _claims) do
    sub = to_string(user.id)
    {:ok, sub}
  end

  def subject_for_token(_, _) do
    {:error, :reason_for_error}
  end

  def resource_from_claims(claims) do
    id = claims["sub"]
    user = Imcon.Auth.get_user(id)
    {:ok, user}
  end

  def resource_from_claims(_claims) do
    {:error, :reason_for_error}
  end

end

#14

I tried this way and that, it turns out that this error occurs when trying to remake authentification under Gardian 1. I did it on the basis of a working application, but I mistook something somewhere, most likely. https://github.com/danbee/chess

He took an example at random, thought it would be easier to build on it. Accordingly, several functions in the new Guardian work differently, so the rules as best they could, so that during the compilation they would not swear.

I read the forum, this is an old problem, how to organize authentication and authorization. In the Phoenix from the box it is not, as for example in Django.