Trying to get user.id into create changeset for rest api

Hi,

I have a rest api project and set it up with guardian and guardianDB. I can create a secret but I am having a hard time figuring out how to get the user.id inserted into the changeset when a secret is created so that it fills the user_id foreign_key column. I tried the solutions proposed in https://elixirforum.com/t/best-way-to-access-user-id-inside-changeset-methods-and-data-models/14055 including OPs original setup, but have had no success and run into this exception.

[info] Sent 500 in 19ms
[error] #PID<0.566.0> running Phoenix.Endpoint.SyncCodeReloadPlug (connection #PID<0.565.0>, stream id 1) terminated
Server: localhost:4000 (http)
Request: POST /api/secret/create
** (exit) an exception was raised:
    ** (UndefinedFunctionError) function ApiWeb.SecretController.create/2 is undefined or private
(api 0.1.0) BeApiWeb.SecretController.create(%Plug.Conn{adapter: {Plug.Cowboy.Conn, :...}, assigns: %{user: %BeApi.Users.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: "258994f3-e1ad-418b-8795-3945f4f9e8c4", username: "Yangg", first_name: nil, last_name: nil, passphrase: "$argon2id$v=19$m=65536,t=8,p=2$ejZm2k+2CCwhFctBcYn+Rw$X1ed7rMLXi0NAL7DnlWwQOQ7sN+1qBI6RHo39YuyIO0", email: "yang@keasy.com", phone_num: nil, role_id: nil, org_id: nil, org: #Ecto.Association.NotLoaded<association :org is not loaded>, secret: #Ecto.Association.NotLoaded<association :secret is not loaded>, inserted_at: ~N[2023-04-16 07:01:25], updated_at: ~N[2023-04-16 07:01:25]}}, body_params: %{"secret" => %{"passphrase" => "Sec7", "secret_name" => "Sec7", "username" => "Sec7"}}, cookies: %{"_be_api_key" => "SFMyNTY.g3QAAAABbQAAAAd1c2VyX2lkbQAAACQyNTg5OTRmMy1lMWFkLTQxOGItODc5NS0zOTQ1ZjRmOWU4YzQ.7DSstKgbgLp-VBo7dOOpaGR4mEZx8QhYXm_B5iltzr4"}, halted: false, host: "localhost", method: "POST", owner: #PID<0.566.0>, params: %{"secret" => %{"passphrase" => "Sec7", "secret_name" => "Sec7", "username" => "Sec7"}}, path_info: ["api", "secret", "create"], path_params: %{}, port: 4000, private: %{BeApiWeb.Router => [], :before_send => [#Function<0.84243074/1 in Plug.Session.before_send/2>, #Function<0.11807388/1 in Plug.Telemetry.call/2>], :guardian_default_claims => %{"aud" => "be_api", "exp" => 1681724422, "iat" => 1681717222, "iss" => "be_api", "jti" => "215008c5-2113-436e-bbfc-644640cbb08d", "nbf" => 1681717221, "sub" => "258994f3-e1ad-418b-8795-3945f4f9e8c4", "typ" => "access"}, :guardian_default_resource => %BeApi.Users.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: "258994f3-e1ad-418b-8795-3945f4f9e8c4", username: "Yangg", first_name: nil, last_name: nil, passphrase: "$argon2id$v=19$m=65536,t=8,p=2$ejZm2k+2CCwhFctBcYn+Rw$X1ed7rMLXi0NAL7DnlWwQOQ7sN+1qBI6RHo39YuyIO0", email: "yang@keasy.com", phone_num: nil, role_id: nil, org_id: nil, org: #Ecto.Association.NotLoaded<association :org is not loaded>, secret: #Ecto.Association.NotLoaded<association :secret is not loaded>, inserted_at: ~N[2023-04-16 07:01:25], updated_at: ~N[2023-04-16 07:01:25]}, :guardian_default_token => "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJrenlfYmVfYXBpIiwiZXhwIjoxNjgxNzI0NDIyLCJpYXQiOjE2ODE3MTcyMjIsImlzcyI6Imt6eV9iZV9hcGkiLCJqdGkiOiIyMTUwMDhjNS0yMTEzLTQzNmUtYmJmYy02NDQ2NDBjYmIwOGQiLCJuYmYiOjE2ODE3MTcyMjEsInN1YiI6IjI1ODk5NGYzLWUxYWQtNDE4Yi04Nzk1LTM5NDVmNGY5ZThjNCIsInR5cCI6ImFjY2VzcyJ9.AAwnbkhOCSfKCqc0JEI-8ckbNMb72koqfZyUdy6JYVKvUdfg8XEYNXnZBGUBPpdfmctmLYsRUmYUu49F6y6e0Q", :guardian_error_handler => BeApiWeb.Auth.GuardianErrorHandler, :guardian_module => BeApiWeb.Auth.Guardian, :phoenix_action => :create, :phoenix_controller => BeApiWeb.SecretController, :phoenix_endpoint => BeApiWeb.Endpoint, :phoenix_format => "json", :phoenix_layout => %{_: {BeApiWeb.LayoutView, :app}}, :phoenix_router => BeApiWeb.Router, :phoenix_view => %{_: BeApiWeb.SecretView}, :plug_session => %{"user_id" => "258994f3-e1ad-418b-8795-3945f4f9e8c4"}, :plug_session_fetch => :done}, query_params: %{}, query_string: "", remote_ip: {127, 0, 0, 1}, req_cookies: %{"_be_api_key" => "SFMyNTY.g3QAAAABbQAAAAd1c2VyX2lkbQAAACQyNTg5OTRmMy1lMWFkLTQxOGItODc5NS0zOTQ1ZjRmOWU4YzQ.7DSstKgbgLp-VBo7dOOpaGR4mEZx8QhYXm_B5iltzr4"}, req_headers: [{"accept", "application/json, text/plain, */*"}, {"accept-encoding", "gzip, deflate, br"}, {"accept-language", "en-US,en;q=0.5"}, {"authorization", "Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJrenlfYmVfYXBpIiwiZXhwIjoxNjgxNzI0NDIyLCJpYXQiOjE2ODE3MTcyMjIsImlzcyI6Imt6eV9iZV9hcGkiLCJqdGkiOiIyMTUwMDhjNS0yMTEzLTQzNmUtYmJmYy02NDQ2NDBjYmIwOGQiLCJuYmYiOjE2ODE3MTcyMjEsInN1YiI6IjI1ODk5NGYzLWUxYWQtNDE4Yi04Nzk1LTM5NDVmNGY5ZThjNCIsInR5cCI6ImFjY2VzcyJ9.AAwnbkhOCSfKCqc0JEI-8ckbNMb72koqfZyUdy6JYVKvUdfg8XEYNXnZBGUBPpdfmctmLYsRUmYUu49F6y6e0Q"}, {"connection", "keep-alive"}, {"content-length", "97"}, {"content-type", "application/json"}, {"cookie", "be_api_key=SFMyNTY.g3QAAAABbQAAAAd1c2VyX2lkbQAAACQyNTg5OTRmMy1lMWFkLTQxOGItODc5NS0zOTQ1ZjRmOWU4YzQ.7DSstKgbgLp-VBo7dOOpaGR4mEZx8QhYXm_B5iltzr4"}, {"host", "localhost:4000"}, {"origin", "moz-extension://cd696cc0-ae39-44bd-99f1-54d9aed2d71b"}, {"sec-fetch-dest", "empty"}, {"sec-fetch-mode", "cors"}, {"sec-fetch-site", "same-origin"}, {"user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0"}], request_path: "/api/secret/create", resp_body: nil, resp_cookies: %{}, resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}, {"x-request-id", "F1apa8pEbeoX_SwAAACh"}], scheme: :http, script_name: [], secret_key_base: :..., state: :unset, status: nil}, %{"secret" => %{"passphrase" => "Sec7", "secret_name" => "Sec7", "username" => "Sec7"}})
        (be_api 0.1.0) lib/be_api_web/controllers/secret_controller.ex:1: BeApiWeb.SecretController.action/2
        (be_api 0.1.0) lib/be_api_web/controllers/secret_controller.ex:1: BeApiWeb.SecretController.phoenix_controller_pipeline/2
        (phoenix 1.7.2) lib/phoenix/router.ex:430: Phoenix.Router.__call__/5
        (be_api 0.1.0) lib/plug/error_handler.ex:80: BeApiWeb.Router.call/2
        (be_api 0.1.0) lib/be_api_web/endpoint.ex:1: BeApiWeb.Endpoint.plug_builder_call/2
        (be_api 0.1.0) lib/plug/debugger.ex:136: BeApiWeb.Endpoint."call (overridable 3)"/2
        (be_api 0.1.0) lib/be_api_web/endpoint.ex:1: BeApiWeb.Endpoint.call/2
        (phoenix 1.7.2) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4
        (plug_cowboy 2.6.1) lib/plug/cowboy/handler.ex:11: Plug.Cowboy.Handler.init/2
        (cowboy 2.9.0) /Users/richardpizano/Desktop/git/elixir/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
        (cowboy 2.9.0) /Users/richardpizano/Desktop/git/elixir/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
        (cowboy 2.9.0) /Users/richardpizano/Desktop/git/elixir/deps/cowboy/src/cowboy_stream_h.erl:295: :cowboy_stream_h.request_process/3
        (stdlib 4.2) proc_lib.erl:240: :proc_lib.init_p_do_apply/3

I have the following user and secret migrations, schemas, and controllers setup.

Schemas
User

defmodule BeApi.Users.User do
  use Ecto.Schema
  import Ecto.Changeset


  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "users" do
    field :username,    :string
    field :first_name,  :string
    field :last_name,   :string
    field :passphrase,  :string
    field :email,       :string
    field :phone_num,   :integer
    field :role_id,     :integer
    belongs_to(:org, BeApi.Orgs.Org, foreign_key: :org_id, on_replace: :delete)
    has_many(:secret, BeApi.Secrets.Secret)

    timestamps()
  end


  def changeset(user, attrs) do
    user
    |> cast(attrs,[
      :username,
      :first_name,
      :last_name,
      :passphrase,
      :email,
      :phone_num
    ])
    |> unique_constraint(:username)
    |> unique_constraint(:email)
    |> validate_required([:email, :passphrase])
    |> validate_format(:email, ~r/@/, message: "Email format not valid")
    |> validate_length(:email, max: 160)
    |> put_password_hash()
  end


  # Takes any user generated passphrase and encrypts it using Argon2
  # this is envoke by '|> put_password_hash()' in the user changeset above
  defp put_password_hash(%Ecto.Changeset{valid?: true, changes:
      %{passphrase: passphrase}} = changeset) do
    change(changeset, passphrase: Argon2.hash_pwd_salt(passphrase))
  end

  defp put_password_hash(changeset), do: changeset
end

Secret

defmodule BeApi.Secrets.Secret do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "secrets" do
    field :secret_name,   :string
    field :username,      :string # username for credentials
    field :passphrase,    :string # passphrase for credentials
    field :file_name,     :string
    field :file_receiver, :string
    field :created_date,  :string
    field :created_by,    :string # user_id table joiner foreign_key
    field :provider,      :string # 1Pass, AWS/ASM, LastPass, etc.
    field :s3_path,       :string
    field :token,         :string
    field :file_location, :string
    belongs_to(:user, BeApi.Users.User, foreign_key: :user_id, on_replace: :delete)

    timestamps()
  end

  @doc false
  def changeset(secret, attrs) do
    secret
    |> cast(attrs, [
      :secret_name,
      :username,
      :passphrase,
      :file_name,
      :file_receiver,
      :created_date,
      :created_by,
      :provider,
      :s3_path,
      :token,
      :file_location,
      :user_id
    ])
    |> unique_constraint(:secret_name)
    |> validate_required(:secret_name)
  end
end

Migrations
User

defmodule BeApi.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users, primary_key: false) do
      add :id, :uuid, primary_key: true
      add :username,    :string
      add :first_name,  :string
      add :last_name,   :string
      add :passphrase,  :string
      add :email,       :string
      add :phone_num,   :integer
      add :role_id,     :integer
      add :org_id, references(:orgs, on_delete: :nothing, type: :binary_id)

      timestamps()
    end

    create unique_index(:users, :username)
    create unique_index(:users, :email)
    create unique_index(:users, :id)
  end
end

Secret

defmodule BeApi.Repo.Migrations.CreateSecrets do
  use Ecto.Migration

  def change do
    create table(:secrets, primary_key: false) do
      add :id, :uuid, primary_key: true
      add :secret_name, :string
      add :username, :string
      add :passphrase, :string
      add :token, :string
      add :file_name, :string
      add :file_receiver, :integer
      add :s3_path, :string
      add :user_id, references(:users, on_delete: :nothing, type: :binary_id)

      timestamps()
    end

    create unique_index(:secrets, :secret_name)
    create unique_index(:secrets, :file_name)
    create unique_index(:secrets, :id)
    create unique_index(:secrets, :user_id)
  end
end

Control Sections for Secret Create that is boiler plate and works - minus adding user.id

def create(conn, %{"secret" => secret_params}) do
    with {:ok, %Secret{} = secret} <- Secrets.create_secret(secret_params) do
      conn
      |> put_status(:created)
      |> render("show.json", secret: secret)
    end
  end

Secret Context file

def update_secret(%Secret{} = secret, attrs \\ %{}) do
    secret
    |> Secret.changeset(attrs)
    |> Repo.update()
  end

Any help is appreciated. This has been driving me mad and I assume I am overlooking something simple.

Your post included a lot of code, but not the source for ApiWeb.SecretController. You’ll get better help if you also post that file.

Based on the message, I suspect your SecretController is missing the override of action/2 that passes the user as a third argument to controller actions; the linked post must have had that set up either explicitly or from a user-authentication library.

Aiming a web request at the create action still tries to call create/2 and gives the error you’re seeing.

Hi, thanks for the reply, here is the full controller file,

defmodule BeApiWeb.SecretController do
  use BeApiWeb, :controller
  alias BeApi.{Secrets, Secrets.Secret}
  import BeApiWeb.Auth.Helpers.CurrentId
  action_fallback BeApiWeb.FallbackController

  plug :is_authorized_user when action in [:update, :delete]

  def index(conn, _params) do
    secrets = Secrets.list_secrets()
    render(conn, "index.json", secrets: secrets)
  end
  
  def create(conn, %{"secret" => secret_params}) do
    with {:ok, %Secret{} = secret} <- Secrets.create_secret(secret_params) do
      conn
      |> put_status(:created)
      |> render("show.json", secret: secret)
    end
  end


  def show(conn, %{"id" => id}) do
    secret = Secrets.get_secret!(id)
    render(conn, "show.json", secret: secret)
  end


  def update(conn, %{"id" => id, "secret" => secret_params}) do
    secret = Secrets.get_secret!(id)

    with {:ok, %Secret{} = secret} <- Secrets.update_secret(secret, secret_params) do
      render(conn, "show.json", secret: secret)
    end
  end


  def delete(conn, %{"id" => id}) do
    secret = Secrets.get_secret!(id)

    with {:ok, %Secret{}} <- Secrets.delete_secret(secret) do
      send_resp(conn, :no_content, "")
    end
  end

It’s not setup to change anything. I went off that linked post and did the |> Helpers.inject_user_id() method as well as the for: user method that was in that post but to no avail could I get a clean secret create or one that inserted the user.id into the changeset. This is the reverted state.

Your module is

but the error says:

This to me says that your router is wrong and has scope "/...", ApiWeb do when it should have scope "/...", BeApiWeb do

Hi, so I was trying to ask for the best implementation to grab the guardian token “sub” data to identify the user_id and then add this to a create changelist so I could do things like associate a secret to the user_id foreign key that created it. This would confirm ownership and be used for permission enforcement when being altered or requested for access. Figured it out so adding my solution for review and to close this issue out on the forum.

def create(conn, %{"secret" => secret_params}) do
    conn = Guardian.Plug.VerifyHeader.call(conn, realm: "Bearer")
    conn = Guardian.Plug.LoadResource.call(conn, allow_blank: true)
    conn = fetch_user_id_from_token(conn, nil)

    changeset = Secret.changeset(%Secret{}, secret_params)
                |> Ecto.Changeset.put_change(:user_id, conn.assigns[:user_id])
    case Repo.insert(changeset) do
      {:ok, secret} ->
        conn
        |> put_status(:created)
        |> render("show.json", secret: secret)
      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> json(%{errors: changeset.errors})
    end
  end


  defp fetch_user_id_from_token(conn, _) do
    case Guardian.Plug.current_resource(conn) do
      nil -> conn
      resource -> assign(conn, :user_id, resource.id)
    end
  end