diff --git a/mix.exs b/mix.exs index dc29baf..9269390 100644 --- a/mix.exs +++ b/mix.exs @@ -19,7 +19,7 @@ defmodule Rumbl.Mixfile do def application do [mod: {Rumbl, []}, applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext, - :phoenix_ecto, :postgrex]] + :phoenix_ecto, :postgrex, :comeonin]] end # Specifies which paths to compile per environment. @@ -37,7 +37,8 @@ defmodule Rumbl.Mixfile do {:phoenix_html, "~> 2.6"}, {:phoenix_live_reload, "~> 1.0", only: :dev}, {:gettext, "~> 0.11"}, - {:cowboy, "~> 1.0"}] + {:cowboy, "~> 1.0"}, + {:comeonin, "~> 2.0"}] end # Aliases are shortcuts or tasks specific to the current project. diff --git a/mix.lock b/mix.lock index d2313ce..2b3aaab 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,5 @@ %{ + "comeonin": {:hex, :comeonin, "2.6.0", "74c288338b33205f9ce97e2117bb9a2aaab103a1811d243443d76fdb62f904ac", [:make, :mix], [], "hexpm", "bc72f049a1c61048427f557821fc06e273abf09f6829377541475d7b36ac8ac6"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "f4763bbe08233eceed6f24bc4fcc8d71c17cfeafa6439157c57349aa1bb4f17c"}, "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm", "db622da03aa039e6366ab953e31186cc8190d32905e33788a1acb22744e6abd2"}, diff --git a/web/controllers/auth.ex b/web/controllers/auth.ex new file mode 100644 index 0000000..9327e6d --- /dev/null +++ b/web/controllers/auth.ex @@ -0,0 +1,41 @@ +defmodule Rumbl.Auth do + import Plug.Conn + import Comeonin.Bcrypt, only: [checkpw: 2, dummy_checkpw: 0] + + def init(opts) do + Keyword.fetch!(opts, :repo) + end + + def call(conn, repo) do + user_id = get_session(conn, :user_id) + user = user_id && repo.get(Rumbl.User, user_id) + assign(conn, :current_user, user) + end + + def login(conn, user) do + conn + |> assign(:current_user, user) + |> put_session(:user_id, user.id) + |> configure_session(renew: true) + end + + def login_by_username_and_password(conn, username, password, opts) do + repo = Keyword.fetch!(opts, :repo) + user = repo.get_by(Rumbl.User, username: username) + + cond do + user && checkpw(password, user.password_hash) -> + {:ok, login(conn, user)} + user -> + {:error, :unauthorized, conn} + true -> + dummy_checkpw() + {:error, :not_found, conn} + end + end + + def logout(conn) do + configure_session(conn, drop: true) # destroy the session, or + # delete_session(conn, :user_id) # to delete only the user_id + end +end diff --git a/web/controllers/session_controller.ex b/web/controllers/session_controller.ex new file mode 100644 index 0000000..6e5ee54 --- /dev/null +++ b/web/controllers/session_controller.ex @@ -0,0 +1,26 @@ +defmodule Rumbl.SessionController do + use Rumbl.Web, :controller + + def new(conn, _) do + render conn, "new.html" + end + + def create(conn, %{"session" => %{"username" => user, "password" => password}}) do + case Rumbl.Auth.login_by_username_and_password(conn, user, password, repo: Repo) do + {:ok, conn} -> + conn + |> put_flash(:info, "Welcome back!") + |> redirect(to: page_path(conn, :index)) + {:error, _reason, conn} -> + conn + |> put_flash(:error, "Invalid username/password combination") + |> render("new.html") + end + end + + def delete(conn, _) do + conn + |> Rumbl.Auth.logout() + |> redirect(to: page_path(conn, :index)) + end +end diff --git a/web/controllers/user_controller.ex b/web/controllers/user_controller.ex index e87ed9a..00173bd 100644 --- a/web/controllers/user_controller.ex +++ b/web/controllers/user_controller.ex @@ -1,6 +1,7 @@ defmodule Rumbl.UserController do use Rumbl.Web, :controller alias Rumbl.User + plug :authenticate when action in [:index, :show] def new(conn, _params) do changeset = User.changeset(%User{}) @@ -8,11 +9,12 @@ defmodule Rumbl.UserController do end def create(conn, %{"user" => user_params}) do - changeset = User.changeset(%User{}, user_params) + changeset = User.registration_changeset(%User{}, user_params) case Repo.insert(changeset) do {:ok, user} -> conn + |> Rumbl.Auth.login(user) |> put_flash(:info, "#{user.name} account created") |> redirect(to: user_path(conn, :index)) {:error, changeset} -> @@ -21,7 +23,7 @@ defmodule Rumbl.UserController do end def index(conn, _params) do - users = Repo.all(Rumbl.User) + users = Repo.all(User) render conn, "index.html", users: users end @@ -29,4 +31,15 @@ defmodule Rumbl.UserController do user = Repo.get(User, id) render conn, "show.html", user: user end + + defp authenticate(conn, _opts) do + if conn.assigns.current_user do + conn + else + conn + |> put_flash(:error, "You must be logged in to access that page") + |> redirect(to: page_path(conn, :index)) + |> halt() + end + end end diff --git a/web/models/user.ex b/web/models/user.ex index 6603c20..f32ee41 100644 --- a/web/models/user.ex +++ b/web/models/user.ex @@ -14,4 +14,20 @@ defmodule Rumbl.User do |> cast(params, ~w(name username), []) |> validate_length(:username, min: 1, max: 20) end + + def registration_changeset(model, params) do + model + |> changeset(params) + |> cast(params, ~w(password), []) + |> validate_length(:password, min: 6, max: 100) + |> put_pass_hash() + end + + defp put_pass_hash(changeset) do + case changeset do + %Ecto.Changeset{valid?: true, changes: %{password: pass}} -> + put_change(changeset, :password_hash, Comeonin.Bcrypt.hashpwsalt(pass)) + _ -> changeset + end + end end diff --git a/web/router.ex b/web/router.ex index 81b6081..64d1810 100644 --- a/web/router.ex +++ b/web/router.ex @@ -7,6 +7,7 @@ defmodule Rumbl.Router do plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers + plug Rumbl.Auth, repo: Rumbl.Repo end pipeline :api do @@ -19,6 +20,7 @@ defmodule Rumbl.Router do get "/", PageController, :index resources "/users", UserController, only: [:new, :create, :index, :show] + resources "/sessions", SessionController, only: [:new, :create, :delete] end # Other scopes may use custom stacks. diff --git a/web/templates/layout/app.html.eex b/web/templates/layout/app.html.eex index c8bef5f..9ca2225 100644 --- a/web/templates/layout/app.html.eex +++ b/web/templates/layout/app.html.eex @@ -13,14 +13,20 @@
<%= get_flash(@conn, :info) %>
<%= get_flash(@conn, :error) %>
diff --git a/web/templates/session/new.html.eex b/web/templates/session/new.html.eex new file mode 100644 index 0000000..3e9e504 --- /dev/null +++ b/web/templates/session/new.html.eex @@ -0,0 +1,11 @@ +