From 374524bb1de2a4e100e9541a1ff2dd62ff28ecbb Mon Sep 17 00:00:00 2001 From: Dan Barber Date: Sat, 20 Jan 2018 22:45:20 -0500 Subject: [PATCH] User can sign in --- lib/chess/{auth => }/auth.ex | 0 lib/chess/auth/error_handler.ex | 12 ++++++ lib/chess/auth/pipeline.ex | 17 ++++++++ lib/chess/auth/user.ex | 5 +++ lib/chess_web.ex | 2 + .../controllers/session_controller.ex | 28 +++++++++++++ lib/chess_web/router.ex | 3 +- lib/chess_web/templates/page/index.html.eex | 2 - lib/chess_web/templates/session/new.html.eex | 21 ++++++++++ lib/chess_web/views/page_view.ex | 3 -- lib/chess_web/views/session_view.ex | 3 ++ mix.exs | 1 + mix.lock | 1 + test/chess/{auth => }/auth_test.exs | 0 .../controllers/game_controller_test.exs | 0 .../controllers/session_controller_test.exs | 8 ++++ test/features/session_test.exs | 39 +++++++++++++++++++ 17 files changed, 139 insertions(+), 6 deletions(-) rename lib/chess/{auth => }/auth.ex (100%) create mode 100644 lib/chess/auth/error_handler.ex create mode 100644 lib/chess/auth/pipeline.ex create mode 100644 lib/chess_web/controllers/session_controller.ex delete mode 100644 lib/chess_web/templates/page/index.html.eex create mode 100644 lib/chess_web/templates/session/new.html.eex delete mode 100644 lib/chess_web/views/page_view.ex create mode 100644 lib/chess_web/views/session_view.ex rename test/chess/{auth => }/auth_test.exs (100%) rename test/{ => chess_web}/controllers/game_controller_test.exs (100%) create mode 100644 test/chess_web/controllers/session_controller_test.exs create mode 100644 test/features/session_test.exs diff --git a/lib/chess/auth/auth.ex b/lib/chess/auth.ex similarity index 100% rename from lib/chess/auth/auth.ex rename to lib/chess/auth.ex diff --git a/lib/chess/auth/error_handler.ex b/lib/chess/auth/error_handler.ex new file mode 100644 index 0000000..988a932 --- /dev/null +++ b/lib/chess/auth/error_handler.ex @@ -0,0 +1,12 @@ +defmodule Chess.Auth.ErrorHandler do + @moduledoc false + + import Plug.Conn + + def auth_error(conn, {type, _reason}, _opts) do + body = to_string(type) + conn + |> put_resp_content_type("text/plain") + |> send_resp(401, body) + end +end diff --git a/lib/chess/auth/pipeline.ex b/lib/chess/auth/pipeline.ex new file mode 100644 index 0000000..16c013d --- /dev/null +++ b/lib/chess/auth/pipeline.ex @@ -0,0 +1,17 @@ +defmodule Chess.Auth.Pipeline do + @moduledoc false + + use Guardian.Plug.Pipeline, + otp_app: :chess, + error_handler: Chess.Auth.ErrorHandler, + module: Chess.Auth.Guardian + + # If there is a session token, validate it + plug Guardian.Plug.VerifySession, claims: %{"typ" => "access"} + + # If there is an authorization header, validate it + plug Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"} + + # Load the user if either of the verifications worked + plug Guardian.Plug.LoadResource, allow_blank: true +end diff --git a/lib/chess/auth/user.ex b/lib/chess/auth/user.ex index 2624306..7927536 100644 --- a/lib/chess/auth/user.ex +++ b/lib/chess/auth/user.ex @@ -14,6 +14,11 @@ defmodule Chess.Auth.User do timestamps() end + def changeset(user) do + user + |> cast(%{}, [:username, :password]) + end + @doc false def changeset(%User{} = user, attrs) do user diff --git a/lib/chess_web.ex b/lib/chess_web.ex index d1c2e38..a8a2ced 100644 --- a/lib/chess_web.ex +++ b/lib/chess_web.ex @@ -40,6 +40,8 @@ defmodule ChessWeb do # Use all HTML functionality (forms, tags, etc) use Phoenix.HTML + import Formulator + import ChessWeb.Router.Helpers import ChessWeb.ErrorHelpers import ChessWeb.Gettext diff --git a/lib/chess_web/controllers/session_controller.ex b/lib/chess_web/controllers/session_controller.ex new file mode 100644 index 0000000..27f4b38 --- /dev/null +++ b/lib/chess_web/controllers/session_controller.ex @@ -0,0 +1,28 @@ +defmodule ChessWeb.SessionController do + use ChessWeb, :controller + + alias Chess.Auth + alias Chess.Auth.User + + def new(conn, _params) do + changeset = User.changeset(%User{}) + render(conn, "new.html", changeset: changeset) + end + + def create( + conn, + %{"user" => %{"username" => username, "password" => password}} + ) do + case Auth.authenticate_user(username, password) do + {:ok, _user} -> + conn + |> put_flash(:info, "You are signed in") + |> redirect(to: game_path(conn, :index)) + {:error, _error} -> + changeset = User.changeset(%User{}) + conn + |> put_flash(:error, "Bad username or password") + |> render("new.html", changeset: changeset) + end + end +end diff --git a/lib/chess_web/router.ex b/lib/chess_web/router.ex index c7769f4..ce0ba67 100644 --- a/lib/chess_web/router.ex +++ b/lib/chess_web/router.ex @@ -17,7 +17,8 @@ defmodule ChessWeb.Router do pipe_through :browser # Use the default browser stack get "/", GameController, :index - resources "/games", GameController, only: [:create, :show, :delete] + resources "/games", GameController, only: [:index, :create, :show, :delete] + resources "/session", SessionController, only: [:new, :create], singleton: true end # Other scopes may use custom stacks. diff --git a/lib/chess_web/templates/page/index.html.eex b/lib/chess_web/templates/page/index.html.eex deleted file mode 100644 index 81d83e2..0000000 --- a/lib/chess_web/templates/page/index.html.eex +++ /dev/null @@ -1,2 +0,0 @@ -
-
diff --git a/lib/chess_web/templates/session/new.html.eex b/lib/chess_web/templates/session/new.html.eex new file mode 100644 index 0000000..235fed1 --- /dev/null +++ b/lib/chess_web/templates/session/new.html.eex @@ -0,0 +1,21 @@ +

Sign in

+ +<%= form_for @changeset, session_path(@conn, :create), [class: "create-session"], fn f -> %> + <%= if @changeset.action do %> +
+

Oops, something went wrong! Please check the errors below.

+
+ <% end %> + +
+
+ <%= input f, :username %> +
+
+ <%= input f, :password, as: :password %> +
+
+
+ <%= submit "Sign in", class: "btn btn-primary" %> +
+<% end %> diff --git a/lib/chess_web/views/page_view.ex b/lib/chess_web/views/page_view.ex deleted file mode 100644 index d2b928b..0000000 --- a/lib/chess_web/views/page_view.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule ChessWeb.PageView do - use ChessWeb, :view -end diff --git a/lib/chess_web/views/session_view.ex b/lib/chess_web/views/session_view.ex new file mode 100644 index 0000000..83ed1ce --- /dev/null +++ b/lib/chess_web/views/session_view.ex @@ -0,0 +1,3 @@ +defmodule ChessWeb.SessionView do + use ChessWeb, :view +end diff --git a/mix.exs b/mix.exs index a4ce3b4..9d1a4fa 100644 --- a/mix.exs +++ b/mix.exs @@ -33,6 +33,7 @@ defmodule Chess.Mixfile do [{:argon2_elixir, "~> 1.2"}, {:comeonin, "~> 4.0"}, {:credo, "~> 0.8", only: [:dev, :test]}, + {:formulator, "~> 0.1.6"}, {:phoenix, "~> 1.3.0"}, {:phoenix_pubsub, "~> 1.0"}, {:phoenix_ecto, "~> 3.0"}, diff --git a/mix.lock b/mix.lock index 2fda73e..5b5ba32 100644 --- a/mix.lock +++ b/mix.lock @@ -14,6 +14,7 @@ "elixir_make": {:hex, :elixir_make, "0.4.0", "992f38fabe705bb45821a728f20914c554b276838433349d4f2341f7a687cddf", [:mix], [], "hexpm"}, "file_system": {:hex, :file_system, "0.2.4", "f0bdda195c0e46e987333e986452ec523aed21d784189144f647c43eaf307064", [:mix], [], "hexpm"}, "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [], []}, + "formulator": {:hex, :formulator, "0.1.6", "266caf4c4bce2b7dc5be2da5e55d8c36120b3eaa6a5f82b9a6c1342ddbc18597", [:mix], [{:gettext, ">= 0.11.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.4", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm"}, "gettext": {:hex, :gettext, "0.14.0", "1a019a2e51d5ad3d126efe166dcdf6563768e5d06c32a99ad2281a1fa94b4c72", [:mix], [], "hexpm"}, "guardian": {:hex, :guardian, "1.0.1", "db0fbaf571c3b874785818b7272eaf5f1ed97a2f9b1f8bc5dc8b0fb8f8f7bb06", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}, {:uuid, ">= 1.1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm"}, "hackney": {:hex, :hackney, "1.10.1", "c38d0ca52ea80254936a32c45bb7eb414e7a96a521b4ce76d00a69753b157f21", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/test/chess/auth/auth_test.exs b/test/chess/auth_test.exs similarity index 100% rename from test/chess/auth/auth_test.exs rename to test/chess/auth_test.exs diff --git a/test/controllers/game_controller_test.exs b/test/chess_web/controllers/game_controller_test.exs similarity index 100% rename from test/controllers/game_controller_test.exs rename to test/chess_web/controllers/game_controller_test.exs diff --git a/test/chess_web/controllers/session_controller_test.exs b/test/chess_web/controllers/session_controller_test.exs new file mode 100644 index 0000000..0222894 --- /dev/null +++ b/test/chess_web/controllers/session_controller_test.exs @@ -0,0 +1,8 @@ +defmodule Chess.SessionControllerTest do + use ChessWeb.ConnCase + + test "shows sign in form", %{conn: conn} do + conn = get conn, session_path(conn, :new) + assert html_response(conn, 200) =~ "Sign in" + end +end diff --git a/test/features/session_test.exs b/test/features/session_test.exs new file mode 100644 index 0000000..a9cef22 --- /dev/null +++ b/test/features/session_test.exs @@ -0,0 +1,39 @@ +defmodule Chess.SessionTest do + use ChessWeb.FeatureCase + + alias Chess.Auth.User + + import Wallaby.Query, only: [text_field: 1, button: 1] + + test "user cannot sign in with incorrect password", %{session: session} do + create_user() + + session + |> visit("/session/new") + |> fill_in(text_field("Username"), with: "link@example.com") + |> fill_in(text_field("Password"), with: "calamityganon") + |> click(button("Sign in")) + + assert session |> has_text?("Bad username or password") + end + + test "user can sign in with correct details", %{session: session} do + create_user() + + session + |> visit("/session/new") + |> fill_in(text_field("Username"), with: "link@example.com") + |> fill_in(text_field("Password"), with: "ilovezelda") + |> click(button("Sign in")) + + assert session |> has_text?("You are signed in") + end + + defp create_user() do + changeset = User.changeset( + %User{}, + %{username: "link@example.com", password: "ilovezelda"} + ) + Repo.insert!(changeset) + end +end