0
votes

I'm working through authentication concepts in a basic Phoenix app. I have a User who has_many emails. A user can login at http://localhost:4000/sessions/new by entering his/her e-mail address. Unfortunately I'm stuck at this point. What do I have to do in auth.ex to fix this?

mix phoenix.new my_app --database mysql
cd my_app
mix phoenix.gen.html User users name:string
mix phoenix.gen.html Email emails value:string user_id:references:users
mix ecto.create
mix ecto.migrate

web/router.ex

[...]
scope "/", MyApp do
  pipe_through :browser # Use the default browser stack

  get "/", PageController, :index
  resources "/users", UserController
  resources "/emails", EmailController
  resources "/sessions", SessionController, only: [:new, :create, :delete]
end
[...]

web/models/user.ex

defmodule MyApp.User do
  use MyApp.Web, :model

  schema "users" do
    field :name, :string
    has_many :emails, MyApp.Email

    timestamps
  end

  @required_fields ~w(name)
  @optional_fields ~w()

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
  end
end

web/models/email.ex

defmodule MyApp.Email do
  use MyApp.Web, :model

  schema "emails" do
    field :value, :string
    belongs_to :user, MyApp.User

    timestamps
  end

  @required_fields ~w(value user_id)
  @optional_fields ~w()

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
  end
end

web/views/session_view.ex

defmodule MyApp.SessionView do
  use MyApp.Web, :view
end

web/templates/session/new.html.eex

<h1>Login</h1>

<%= form_for @conn, session_path(@conn, :create), [as: :session], fn f -> %>
  <div class="form-group">
    <%= text_input f, :email, placeholder: "email" %>
  </div>
  <%= submit "Log in" %>
<% end %>

web/controllers/session_controller.ex

defmodule MyApp.SessionController do
  use MyApp.Web, :controller

  def new(conn, _) do
    render conn, "new.html"
  end

  def create(conn, %{"session" => %{"email" => email}}) do
    case MyApp.Auth.login_by_email(conn, email, repo: Repo) do
      {:ok, conn} ->
        conn
        |> put_flash(:info, "Welcome!")
        |> redirect(to: page_path(conn, :index))
      {:error, _reason, conn} ->
        conn
        |> put_flash(:error, "Invalid email")
        |> render("new.html")
    end
  end
end

web/controllers/auth.ex

defmodule MyApp.Auth do
  import Plug.Conn
  alias MyApp.User

  def init(opts) do
    Keyword.fetch!(opts, :repo)
  end

  def login_by_email(conn, email, _opts) do
    # What code would fix this?
    #
    user = repo.get!(User, email)
    conn
    |> assign(:current_user, user)
    |> put_session(:user_id, user.id)
    |> configure_session(renew: true)
  end
end
2
Can you describe more about what the issue is? Are you not able to find a user based on an email? Or is it something else?vikram7
I'm not able to find the user based on an email.wintermeyer

2 Answers

1
votes

You need to import Ecto.Query for the query to work.

import Ecto.Query

def login_by_email(conn, email, _opts) do
  user = 
    MyApp.User
    |> join(:inner, [u], e in assoc(u, :emails))
    |> where([u, e], e.value == ^email)
    |> MyApp.Repo.one!()    

  conn
  |> assign(:current_user, user)
  |> put_session(:user_id, user.id)
  |> configure_session(renew: true)

end

Also, notice the inner join, which assumes that the emails' values are unique.

0
votes

You can just query the email and preload the user.

email_with_user = 
  repo.one(from e in Email,
           join: u in assoc(e, :user),
           where: e.value == ^email)
user = email_with_user.user

Also, the plug specification says that there has to be init and call function for a plug to work.