📜 ⬆️ ⬇️

Creating a blog engine with Phoenix and Elixir / Part 1. Introduction



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:


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:

 # Configure your database config :pxblog, Pxblog.Repo, adapter: Ecto.Adapters.Postgres, username: "postgres", password: "postgres", database: "pxblog_dev", hostname: "localhost", pool_size: 10 

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 # Virtual Fields field :password, :string, virtual: true field :password_confirmation, :string, virtual: true end 

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


  1. Introduction
  2. Authorization
  3. Adding Roles
  4. Process roles in controllers
  5. We connect ExMachina
  6. Markdown support
  7. Add comments
  8. We finish with comments
  9. Channels
  10. Channel testing
  11. Conclusion


About all inaccuracies, errors, poor translation, please write personal messages, I will promptly correct. Thanks in advance to everyone involved.

Source: https://habr.com/ru/post/311088/


All Articles