
From the translator: “
Elixir and Phoenix are a great example of where modern web development is heading. Already, these tools provide high-quality access to real-time technologies for web applications. Sites with increased interactivity, multiplayer browser games, microservices - those areas in which these technologies will serve a good service. The following is a translation of a series of 11 articles that describe in detail the aspects of development on the Phoenix framework that would seem such a trivial thing as a blog engine. But do not hurry to sulk, it will be really interesting, especially if the articles encourage you to pay attention to the Elixir or to become its followers . ”
At the moment, our application is based on:
- Elixir : v1.3.1
- Phoenix : v1.2.0
- Ecto : v2.0.2
- Comeonin : v2.5.2
Install Phoenix
The best installation instructions for Phoenix is on
its official website .
')
Step 1. Adding posts
Let's start by running the mix task to create a new project called “
pxblog ”. To do this, run the command
`mix phoenix.new [project] [command]` . We answer in the affirmative to all questions, as the default settings will suit us.
mix phoenix.new pxblog
Conclusion:
* creating pxblog/config/config.exs ... Fetch and install dependencies? [Yn] y * running mix deps.get * running npm install && node node_modules/brunch/bin/brunch build We are all set! Run your Phoenix application: $ cd pxblog $ mix phoenix.server You can also run your app inside IEx (Interactive Elixir) as: $ iex -S mix phoenix.server Before moving on, configure your database in config/dev.exs and run: $ mix ecto.create
We should see a lot of information that the creation of our project has been completed, as well as teams for the preparatory work. Perform them one by one.
If you have not created a Postgres database or the application is not configured to work with it, the command
`mix ecto.create` will throw an error. To fix it, open the
config / dev.exs file and just change the username and password for the role that has rights to create the database:
When everything works, let's start the server and make sure that everything is fine.
$ iex -S mix phoenix.server
To do this, go to
http: // localhost: 4000 / and see the
Welcome to Phoenix page
! . Good foundation is ready. Let's add to it the main scaffold for working with posts, since we still have a blog engine.
We use the generator built into Phoenix to create an Ecto-model, a migration, and an interface for processing CRUD operations of the
Post module. Since at the moment this is a
very, very simple engine, we will limit ourselves to the title and the message. The title will be a string, and the message will be text. The team that makes this all for us is pretty simple:
$ mix phoenix.gen.html Post posts title:string body:text
Conclusion:
* creating web/controllers/post_controller.ex ... Add the resource to your browser scope in web/router.ex: resources "/posts", PostController Remember to update your repository by running migrations: $ mix ecto.migrate
We get an error! To fix it, and at the same time make the post interface accessible from the browser, let's open the
web / router.ex file , and add the following line to the root-scop:
resources "/posts", PostController
Now we can once again execute the command and make sure that our migration was successful.
$ mix ecto.migrate
Conclusion:
Compiling 9 files (.ex) Generated pxblog app 15:52:20.004 [info] == Running Pxblog.Repo.Migrations.CreatePost.change/0 forward 15:52:20.004 [info] create table posts 15:52:20.019 [info] == Migrated in 0.0s
And finally, we restart our server go to
http: // localhost: 4000 / posts , where we should see the
Listing posts header, as well as a list of our posts.
A little fidgeting, we were able to add new posts, edit them and delete. Pretty
cool for such a small job!
Step 1B. Writing tests for posts
One of the charms of working with scaffolds is that from the very beginning we get a set of basic tests. We don’t even need to change them much until we start making major changes to the code of the application itself. Now let's look at what tests were created to better understand how to write your own.
First, open the
test / models / post_test.exs file and take a look at the contents:
defmodule Pxblog.PostTest do use Pxblog.ModelCase alias Pxblog.Post @valid_attrs %{body: "some content", title: "some content"} @invalid_attrs %{} test "changeset with valid attributes" do changeset = Post.changeset(%Post{}, @valid_attrs) assert changeset.valid? end test "changeset with invalid attributes" do changeset = Post.changeset(%Post{}, @invalid_attrs) refute changeset.valid? end end
Let's sort this code in parts to understand what is going on here.
defmodule Pxblog.PostTest do
Obviously, we need to define a test module in the namespace of our application.
use Pxblog.ModelCase
Next, we tell this module to use the functions and DSLs presented in the
ModelCase macro
set .
alias Pxblog.Post
Now we are convinced that the test can access the model directly.
@valid_attrs% {body: “some content”, title: “some content”}
We configure the main valid attributes that will allow you to successfully create a
changeset . This is just a module level variable that we can pull every time we want to create a valid model.
@invalid_attrs% {}
As above, but create a set of invalid attributes.
test "changeset with valid attributes" do
changeset = Post.changeset (% Post {}, @valid_attrs)
assert changeset.valid?
end
Now, we directly create our test with the
test function, giving it a string name. Inside the body of our function, we first create a revision from the
Post model (passing an empty structure and a list of valid parameters). Then, using the
assert function, we check the validity of the revision. This is what we need the
@valid_attrs variable
for .
test "changeset with invalid attributes" do
changeset = Post.changeset (% Post {}, @invalid_attrs)
refute changeset.valid?
end
Finally, we check the creation of a revision with invalid parameters, and instead of
“asserting” that the audit is a valid one, we perform the reverse operation
refute .
This is a very good example of how to write a test model. Now let's look at the test controller. Looking at him, we should see something like the following:
defmodule Pxblog.PostControllerTest do use Pxblog.ConnCase alias Pxblog.Post @valid_attrs %{body: "some content", title: "some content"} @invalid_attrs %{} ... end
Here
Pxblog.ConnCase is used to obtain the controller-specific DSL level. The remaining lines should already be familiar.
Let's look at the first test:
test "lists all entries on index", %{conn: conn} do conn = get conn, post_path(conn, :index) assert html_response(conn, 200) =~ "Listing posts" end
Here we capture the variable
conn , which must be sent via the
tuner to
ConnCase . I will explain this later. The next step is to use the function of the same name with the appropriate HTTP method to make a request on the desired path (
GET request to action
: index in our case). Then we check that the response of this action returns HTML with a status of
200 (“OK”) and contains the phrase
Listing posts .
test "renders form for new resources", %{conn: conn} do conn = get conn, post_path(conn, :new) assert html_response(conn, 200) =~ "New post" end
The following test is essentially the same, only the action
new is being tested. It's simple.
test "creates resource and redirects when data is valid", %{conn: conn} do conn = post conn, post_path(conn, :create), post: @valid_attrs assert redirected_to(conn) == post_path(conn, :index) assert Repo.get_by(Post, @valid_attrs) end
And here we are doing something new. First, we send a POST request with a list of valid parameters to
post_path . We expect to receive a redirect to the list of posts (action
: index ). The
redirected_to function accepts a connection object as an argument, since we need to know where the redirection occurred.
Finally, we claim that the object represented by these valid parameters was successfully added to the database. This check is carried out through a request to the
Ecto Repo repository to search for the
Post model that corresponds to our
@valid_attrs parameters.
test "does not create resource and renders errors when data is invalid", %{conn: conn} do conn = post conn, post_path(conn, :create), post: @invalid_attrs assert html_response(conn, 200) =~ "New post" end
Now we will try to create a post again, but with an invalid parameter list
@invalid_attrs , and check that the post creation form is displayed again.
test "shows chosen resource", %{conn: conn} do post = Repo.insert! %Post{} conn = get conn, post_path(conn, :show, post) assert html_response(conn, 200) =~ "Show post" end
To test the
show action, we need to create a
Post model, with which we will work. Then we call the
get function with the
post_path helper, and make sure that the corresponding resource is returned.
We can also try to request the path to a resource that does not exist as follows:
test "renders page not found when id is nonexistent", %{conn: conn} do assert_error_sent 404, fn -> get conn, post_path(conn, :show, -1) end end
It uses a different test record pattern, which is actually quite simple to understand. We describe the expectation that a request for a non-existent resource will result in error 404. We also pass in an anonymous function containing the code, which, when executed, should return this error itself. It's simple!
The remaining tests just repeat the above for the remaining paths. But on the distance we will stop in more detail:
test "deletes chosen resource", %{conn: conn} do post = Repo.insert! %Post{} conn = delete conn, post_path(conn, :delete, post) assert redirected_to(conn) == post_path(conn, :index) refute Repo.get(Post, post.id) end
In general, everything is similar, except for using the HTTP
delete method. We
state that we should be redirected from the delete page back to the list of posts. We also use a new feature here -
“reject” the existence of the
Post object using the
refute function.
Step 2. Add Users
To create a
User model, we will go through almost the same steps as when creating a post model, with the exception of adding other columns. For a start, let's execute:
$ mix phoenix.gen.html User users username:string email:string password_digest:string
Conclusion:
* creating web/controllers/user_controller.ex ... Add the resource to your browser scope in web/router.ex: resources "/users", UserController Remember to update your repository by running migrations: $ mix ecto.migrate
Next, open the
web / router.ex file and add the following line to the same scopes as before:
resources "/users", UserController
The syntax here defines the standard resource path, where the first argument is the URL, and the second is the name of the controller class. Then execute:
$ mix ecto.migrate
Conclusion:
Compiling 11 files (.ex) Generated pxblog app 16:02:03.987 [info] == Running Pxblog.Repo.Migrations.CreateUser.change/0 forward 16:02:03.987 [info] create table users 16:02:03.996 [info] == Migrated in 0.0s
Finally, we reboot the server and check
http: // localhost: 4000 / users . Now, in addition to posts, we have the ability to add users!
Unfortunately, this is not a very useful blog yet. In the end, even though we can create users (
unfortunately, now anyone can do this ), we cannot even enter. In addition, the password digest does not use any encryption algorithms. We stupidly store the text that the user entered! Not cool at all!
Let's bring this screen into a look more like a registration.
Our user tests look exactly the same as what was automatically generated for our posts, so until we leave them alone, until we begin to modify the logic (
and we’ll do this right now! ).
Step 3. Saving the password hash instead of the password itself
Having opened the address
/ users / new , we see three fields:
Username ,
Email and
PasswordDigest . But when you register on other sites, you are not asked to enter a password digest, but the password itself, along with its confirmation! How can we fix this?
In the
web / templates / user / form.html.eex file, delete the following lines:
<div class="form-group"> <%= label f, :password_digest, class: "control-label" %> <%= text_input f, :password_digest, class: "form-control" %> <%= error_tag f, :password_digest %> </div>
And add in their place:
<div class="form-group"> <%= label f, :password, "Password", class: "control-label" %> <%= password_input f, :password, class: "form-control" %> <%= error_tag f, :password %> </div> <div class="form-group"> <%= label f, :password_confirmation, "Password Confirmation", class: "control-label" %> <%= password_input f, :password_confirmation, class: "form-control" %> <%= error_tag f, :password_confirmation %> </div>
After updating the page (should occur automatically) enter the user data and click
Submit .
Oops, error:
Oops, something went wrong! Please check the errors below.
This is because we use the password and password confirmation fields, which the application knows nothing about. Let's write code that solves this problem.
Let's start by changing the schema. In the
web / models / user.ex file, add a couple of lines:
schema "users" do field :username, :string field :email, :string field :password_digest, :string timestamps
Note the addition of two fields
: password and
: password_confirmation . We declared them as virtual fields, since in fact they do not exist in our database, but they must exist as properties in the
User structure. It also allows you to apply transformations in our
changeset function.
Then we add
: password and
: password_confirmation to the list of required fields:
def changeset(struct, params \\ %{}) do struct |> cast(params, [:username, :email, :password, :password_confirmation]) |> validate_required([:username, :email, :password, :password_confirmation]) end
If you now try to run tests from the file
test / models / user_test.exs , then the
“changeset with valid attributes” test will fall. This is because we added
: password and
: password_confirmation to the required parameters, but did not update
@valid_attrs . Let's change this line:
@valid_attrs %{email: "[email protected]", password: "test1234", password_confirmation: "test1234", username: "testuser"}
Our model tests must pass again! Now we need to repair the tests of the controllers Let's make some changes to the
test / controllers / user_controller_test.exs file . First, select the valid attributes to create an object in a separate variable:
@valid_create_attrs %{email: "[email protected]", password: "test1234", password_confirmation: "test1234", username: "testuser"} @valid_attrs %{email: "[email protected]", username: "testuser"}
Then change our user creation test:
test "creates resource and redirects when data is valid", %{conn: conn} do conn = post conn, user_path(conn, :create), user: @valid_create_attrs assert redirected_to(conn) == user_path(conn, :index) assert Repo.get_by(User, @valid_attrs) end
And the user update test:
test "updates chosen resource and redirects when data is valid", %{conn: conn} do user = Repo.insert! %User{} conn = put conn, user_path(conn, :update, user), user: @valid_create_attrs assert redirected_to(conn) == user_path(conn, :show, user) assert Repo.get_by(User, @valid_attrs) end
After our tests
turn green again, we need to add a function to the
changeset that converts the password into a digest:
def changeset(struct, params \\ %{}) do struct |> cast(params, [:username, :email, :password, :password_confirmation]) |> validate_required([:username, :email, :password, :password_confirmation]) |> hash_password end defp hash_password(changeset) do changeset |> put_change(:password_digest, "ABCDE") end
For now, we simply stabilize the behavior of our hashing function. First of all, let's make sure that the revision change happens correctly. Let's go back to the browser at
http: // localhost: 4000 / users , click on the
New user link and create a new user with any data. Now a new line awaits us in the list of users, in which the password digest is
ABCDE .
Run the tests for this file again. They pass, but there is not enough test to test the function
hash_password . Let's add:
test "password_digest value gets set to a hash" do changeset = User.changeset(%User{}, @valid_attrs) assert get_change(changeset, :password_digest) == "ABCDE" end
This is a big step forward for the application, but not so much for safety! We need to fix the password hashing for the present with the use of
BCrypt , kindly provided by the
Comeonin library.
To do this, open the
mix.exs file and add
: comeonin to the
applications list:
def application do [mod: {Pxblog, []}, applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext, :phoenix_ecto, :postgrex, :comeonin]] end
We also need to change our dependencies. Pay attention to
{: comeonin, “~> 2.3”} :
defp deps do [{:phoenix, "~> 1.2.0"}, {:phoenix_pubsub, "~> 1.0"}, {:phoenix_ecto, "~> 3.0"}, {:postgrex, ">= 0.0.0"}, {:phoenix_html, "~> 2.6"}, {:phoenix_live_reload, "~> 1.0", only: :dev}, {:gettext, "~> 0.11"}, {:cowboy, "~> 1.0"}, {:comeonin, "~> 2.3"}] end
Now turn off the running server and run the command
`mix deps.get` . If everything goes well (
and it should! ), Then the command
`iex -S mix phoenix.server` you can again start the server.
Our old
hash_password method
is not bad , but generally we need the password to be hashed. Since we have added the
Comeonin library, which provides us with an excellent
Bcrypt module with the
hashpwsalt method, which we import into our
User model. In the
web / models / user.ex file, add the line below right after the
use of Pxblog.Web,: model :
import Comeonin.Bcrypt, only: [hashpwsalt: 1]
What have we done now? We pulled the
Bcrypt module out of the
Comeonin namespace and imported the
hashpwsalt method with the arity of 1. And with the following code, we make the
hash_password function work:
defp hash_password(changeset) do if password = get_change(changeset, :password) do changeset |> put_change(:password_digest, hashpwsalt(password)) else changeset end end
I suggest trying to create a user again! This time, after registration, we should see an encrypted digest in the
password_digest field!
Now let's modify the
hash_password function a
little . First, in order for password encryption not to inhibit testing, it is necessary to make changes to the settings of the test environment. To do this, open the
config / test.exs file and add the following line to the bottom:
config :comeonin, bcrypt_log_rounds: 4
This will tell
Comeonin not to encrypt the password too much during the execution of tests, because in tests, speed is more important to us than security! And in production (the
config / prod.exs file ), on the contrary, we need to strengthen the protection:
config :comeonin, bcrypt_log_rounds: 14
Let's write a test to call
Comeonin . We will make it less detailed, as we just want to make sure that the encryption works. In the
test / models / user_test.exs file :
test "password_digest value gets set to a hash" do changeset = User.changeset(%User{}, @valid_attrs) assert Comeonin.Bcrypt.checkpw(@valid_attrs.password, Ecto.Changeset.get_change(changeset, :password_digest)) end
To improve test coverage, let's consider the case where the line
`if the password = get_change () is not true:
test "password_digest value does not get set if password is nil" do changeset = User.changeset(%User{}, %{email: "[email protected]", password: nil, password_confirmation: nil, username: "test"}) refute Ecto.Changeset.get_change(changeset, :password_digest) end
In this case, the
password_digest field must remain empty, which is what happens! We do a good job covering our code with tests!
Step 4. Let's get in!
Add a new
SessionController controller and a related
SessionView view . Let's start with a simple, and eventually we will come to a more correct implementation.
Create a
web / controllers / session_controller.ex file:
defmodule Pxblog.SessionController do use Pxblog.Web, :controller def new(conn, _params) do render conn, "new.html" end end
As well as
web / views / session_view.ex :
defmodule Pxblog.SessionView do use Pxblog.Web, :view end
And finally
web / templates / session / new.html.eex :
<h2>Login</h2>
Add the following line to the scop
"/" :
resources "/sessions", SessionController, only: [:new]
Thus, we include our new controller in the router. The only path we need now is
new , which we explicitly specify. Again, we need to get the most stable foundation with the most simple methods.
Going to
http: // localhost: 4000 / sessions / new , we should see the
Login subtitle under the heading
Phoenix framework .
Add this form here. To do this, create a file
web / templates / session / form.html.eex :
<%= form_for @changeset, @action, fn f -> %> <%= if f.errors != [] do %> <div class="alert alert-danger"> <p>Oops, something went wrong! Please check the errors below:</p> <ul> <%= for {attr, message} <- f.errors do %> <li><%= humanize(attr) %> <%= message %></li> <% end %> </ul> </div> <% end %> <div class="form-group"> <label>Username</label> <%= text_input f, :username, class: "form-control" %> </div> <div class="form-group"> <label>Password</label> <%= password_input f, :password, class: "form-control" %> </div> <div class="form-group"> <%= submit "Submit", class: "btn btn-primary" %> </div> <% end %>
And let's just create a form in the
web / templates / session / new.html.eex file with just one line:
<%= render "form.html", changeset: @changeset, action: session_path(@conn, :create) %>
Thanks to the automatic reload of the code, an error will appear on the page, since we have not yet defined the
@changeset variable, which, as you might guess, should be a revision. Since we are working with an object that has fields
: name and
: password , let's use them!
In the
web / controllers / session_controller.ex file, we need to add an alias of the
User model so that we can safely access it further. At the top of our class, just below the line
use Pxblog.Web,: controller add the following:
alias Pxblog.User
And in the
new function, change the render call, as shown below:
render conn, "new.html", changeset: User.changeset(%User{})
We must pass here the connection object, the template that we want to render (without the eex extension) and a list of additional variables that will be used inside the template. In this case, we need to specify a
changeset: and pass it an
Ecto revision for
User with an empty user structure.
Update the page. Now we should see another error that looks like this:
No helper clause for Pxblog.Router.Helpers.session_path/2 defined for action :create. The following session_path actions are defined under your router: *:new
In our form, we refer to a path that does not yet exist. We use the
session_path helper, passing it the
@conn object, but then we specify the path
: create , which is yet to be created.
Half way passed.
Now let's implement the real login feature using the session. For this we change our ways.In the web / router.ex file, include : create in the SessionController description : resources "/sessions", SessionController, only: [:new, :create]
In the file web / controllers / session_controller.ex import function checkpw of module Bcrypt library Comeonin : import Comeonin.Bcrypt, only: [checkpw: 2]
This line says “Import only the checkpw function with arity 2" from the Comeonin.Bcrypt module .And then connect the scrub_params plug - in that will work with user data. Add to our functions: plug :scrub_params, "user" when action in [:create]
scrub_params is a special function that clears user input. In the case when, for example, an attribute is passed as an empty string, scrub_params will convert it to nil to avoid creating records with empty rows in the database.Next we add a function to handle the create action . Place it at the bottom of the SessionController module . There will be a lot of code here, so let's break it down in parts.In the web / controllers / session_controller.ex file : def create(conn, %{"user" => user_params}) do Repo.get_by(User, username: user_params["username"]) |> sign_in(user_params["password"], conn) end
Repo.get_by(User, username: user_params[“username”]) User Ecto Repo ,
username nil .
, :
iex(3)> Repo.get_by(User, username: "flibbity") [debug] SELECT u0."id", u0."username", u0."email", u0."password_digest", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."username" = $1) ["flibbity"] OK query=0.7ms nil iex(4)> Repo.get_by(User, username: "test") [debug] SELECT u0."id", u0."username", u0."email", u0."password_digest", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."username" = $1) ["test"] OK query=0.8ms %Pxblog.User{__meta__: %Ecto.Schema.Metadata{source: "users", state: :loaded}, email: "test", id: 15, inserted_at: %Ecto.DateTime{day: 24, hour: 19, min: 6, month: 6, sec: 14, usec: 0, year: 2015}, password: nil, password_confirmation: nil, password_digest: "$2b$12$RRkTZiUoPVuIHMCJd7yZUOnAptSFyM9Hw3Aa88ik4erEsXTZQmwu2", updated_at: %Ecto.DateTime{day: 24, hour: 19, min: 6, month: 6, sec: 14, usec: 0, year: 2015}, username: "test"}
,
sign_in . , !
defp sign_in(user, password, conn) when is_nil(user) do conn |> put_flash(:error, "Invalid username/password combination!") |> redirect(to: page_path(conn, :index)) end defp sign_in(user, password, conn) do if checkpw(password, user.password_digest) do conn |> put_session(:current_user, %{id: user.id, username: user.username}) |> put_flash(:info, "Sign in successful!") |> redirect(to: page_path(conn, :index)) else conn |> put_session(:current_user, nil) |> put_flash(:error, "Invalid username/password combination!") |> redirect(to: page_path(conn, :index)) end end
The main thing to pay attention to is the order in which these functions are defined. The first of them has a protection condition, therefore this method will be performed only in the case of the truth of this condition. So if we didn’t find the user, we’ll redirect back to root_path with the appropriate message.The second function will handle all other scripts (when the guard condition is false). We check the password with the checkpw function . If it is correct, then we write the user to the current_user session variable and redirect with a message about successful login. Otherwise, we clear the current user session, set the error message, and redirect to the root.We can go to http: // localhost: 4000 / sessions / new and check how it works. With the correct data we will go inside, and with the wrong we get an error.We also need to write tests for this controller. Create a file test / controllers / session_controller_test.exs and fill it with the following code: defmodule Pxblog.SessionControllerTest do use Pxblog.ConnCase alias Pxblog.User setup do User.changeset(%User{}, %{username: "test", password: "test", password_confirmation: "test", email: "[email protected]"}) |> Repo.insert {:ok, conn: build_conn()} end test "shows the login form", %{conn: conn} do conn = get conn, session_path(conn, :new) assert html_response(conn, 200) =~ "Login" end test "creates a new user session for a valid user", %{conn: conn} do conn = post conn, session_path(conn, :create), user: %{username: "test", password: "test"} assert get_session(conn, :current_user) assert get_flash(conn, :info) == "Sign in successful!" assert redirected_to(conn) == page_path(conn, :index) end test "does not create a session with a bad login", %{conn: conn} do conn = post conn, session_path(conn, :create), user: %{username: "test", password: "wrong"} refute get_session(conn, :current_user) assert get_flash(conn, :error) == "Invalid username/password combination!" assert redirected_to(conn) == page_path(conn, :index) end test "does not create a session if user does not exist", %{conn: conn} do conn = post conn, session_path(conn, :create), user: %{username: "foo", password: "wrong"} assert get_flash(conn, :error) == "Invalid username/password combination!" assert redirected_to(conn) == page_path(conn, :index) end end
We start with the standard setup block and a fairly basic GET request check. The creation test looks more interesting: test "creates a new user session for a valid user", %{conn: conn} do conn = post conn, session_path(conn, :create), user: %{username: "test", password: "test"} assert get_session(conn, :current_user) assert get_flash(conn, :info) == "Sign in successful!" assert redirected_to(conn) == page_path(conn, :index) end
The first line is to send a POST request for the session creation path. Then there are checks whether the current_user session variable was set , a login message appeared, and finally, whether the redirection took place. In the rest of the tests, we also check other ways where the sign_in function can go . Again, everything is very simple!Step 5. Improving our current_user
Let's change the main template so that it displays either the username or a link to the input, depending on whether the user is logged in or not.To do this, add a helper to the web / views / layout_view.ex file , which will make it easier to get information about the current user: def current_user(conn) do Plug.Conn.get_session(conn, :current_user) end
Now open the web / templates / layout / app.html.eex file and add the following instead of the Get Started link : <li> <%= if user = current_user(@conn) do %> Logged in as <strong><%= user.username %></strong> <br> <%= link "Log out", to: session_path(@conn, :delete, user.id), method: :delete %> <% else %> <%= link "Log in", to: session_path(@conn, :new) %> <% end %> </li>
Let's break it down again in steps. One of the first things we need to do is find out who the current user is, assuming that he is already logged in. First, we will make a decision in the forehead, and we will deal with refactoring later. Install the user from the session directly in our template. The get_session function is part of a Conn object .If the user is logged in, we need to show him a link to the output. We will treat the session as a normal resource, so to exit, we will simply delete the session using the link to this action.We also need to display the name of the current user. We store the user structure in the current_user session variable , so we have the opportunity to get usernamevia user.username .If we could not find the user, then simply show the link to the entrance. Here, we again consider the session as a resource, so new will provide the correct way to create a new session.You probably noticed that after updating the page we get another message with an error about the missing function. Let's connect the required path to make Phoenix happy!In the web / router.ex file, we also add to the session routes : delete : resources "/sessions", SessionController, only: [:new, :create, :delete]
Still need to change the controller. In the web / controllers / session_controller.ex file, add the following: def delete(conn, _params) do conn |> delete_session(:current_user) |> put_flash(:info, "Signed out successfully!") |> redirect(to: page_path(conn, :index)) end
Since we have just deleted the current_user key , it doesn’t matter what parameters come in, so we mark them with an underscore at first as unused. We also set a message about a successful exit and redirected to the list of posts.Now we can log in, log out and check for unsuccessful logins. Everything goes for the better! But first we need to write a few tests. We will start with tests for our LayoutView . The first thing we are going to do is to set aliases for the LayoutView and User modules to shorten the code. Next, in the setup block, we create a user and add it to the database. And then return the standard tuple {: ok, conn: build_conn ()} . defmodule Pxblog.LayoutViewTest do use Pxblog.ConnCase, async: true alias Pxblog.LayoutView alias Pxblog.User setup do User.changeset(%User{}, %{username: "test", password: "test", password_confirmation: "test", email: "[email protected]"}) |> Repo.insert {:ok, conn: build_conn()} end test "current user returns the user in the session", %{conn: conn} do conn = post conn, session_path(conn, :create), user: %{username: "test", password: "test"} assert LayoutView.current_user(conn) end test "current user returns nothing if there is no user in the session", %{conn: conn} do user = Repo.get_by(User, %{username: "test"}) conn = delete conn, session_path(conn, :delete, user) refute LayoutView.current_user(conn) end end
Now consider the tests themselves. In the first of these, we create a session and state that the LayoutView.current_user function should return certain data. In the second we will consider the opposite situation. We explicitly delete the session and refute that the current_user function returns the user.We also added a delete action to SessionController , therefore, we also need to write a test for this: test "deletes the user session", %{conn: conn} do user = Repo.get_by(User, %{username: "test"}) conn = delete conn, session_path(conn, :delete, user) refute get_session(conn, :current_user) assert get_flash(conn, :info) == "Signed out successfully!" assert redirected_to(conn) == page_path(conn, :index) end
Here we make sure that the current_user from the session is empty, and also we check the returned flash message and redirection.On it the first part came to an end.An important conclusion from the translator
I have done a great job of translating both this article and the translation of the entire series. What I continue to do now. Therefore, if you liked the article itself or initiatives in popularizing Elixir in runet, please support the article with pluses, comments and reposts. This is incredibly important both for me personally and for the whole Elixir community as a whole.Other series articles
- Introduction
- Authorization
- Adding Roles
- Process roles in controllers
- We connect ExMachina
- Markdown support
- Add comments
- We finish with comments
- Channels
- Channel testing
- Conclusion
About all inaccuracies, errors, poor translation, please write personal messages, I will promptly correct. Thanks in advance to everyone involved.