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.