Escrevendo um Blog Engine em Phoenix e Elixir: Parte 1

Atualizado em : 20/07/2016

Versões atuais:

A partir do momento em que escreveu isso, as versões atuais de nossas aplicações são:

  • Elixir : v1.3.1
  • Phoenix: v1.2.0
  • Ecto: v2.0.2
  • Comeonin: v2.5.2

Se você está lendo isso e estes não são os últimos, me avise e atualizarei este tutorial de acordo.

Instalando Phoenix

As melhores instruções para instalar a Phoenix podem ser encontradas no site da Phoenix.

Passo 1: Vamos adicionar postagens

Precisamos começar usando a tarefa Phoenix mix para criar um novo projeto, que chamaremos "pxblog". Fazemos isso usando o mix phoenix.new [project] [command]. Responda Y a todas as perguntas, pois queremos usar phoenix.js e os outros requisitos de front-end.

 mix phoenix.new pxblog 

Saída:

 * criando pxblog / config / config.exs 
...
 Obter e instalar dependências? [Yn] y 
* executando mix deps.get
* executando npm install && node node_modules / brunch / bin / brunch build
 Estamos todos prontos! Execute o seu aplicativo Phoenix: 
 $ cd pxblog 
$ mix phoenix.server
 Você também pode executar seu aplicativo dentro de IEx (Elixir Interativo) como: 
 $ iex -S mix phoenix.server 
 Antes de seguir em frente, configure seu banco de dados em config / dev.exs e execute: 
 $ mix ecto.create 

Devemos ver um monte de resultados indicando que nosso projeto foi concluído e o trabalho inicial está pronto.

O passo mix ecto.create pode falhar se não configurarmos nosso banco de dados postgres corretamente ou configurado nosso aplicativo para usar as credenciais corretas. Se você abrir config / dev.exs , você deve ver alguns detalhes de configuração na parte inferior do arquivo:

 # Configure seu banco de dados 
config: pxblog, Pxblog.Repo,
adaptador: Ecto.Adapters.Postgres,
nome de usuário: "postgres",
senha: "postgres",
banco de dados: "pxblog_dev",
nome do host: "localhost",
pool_size: 10

Basta alterar o nome de usuário e a senha para uma função que tenha as permissões de criação de banco de dados corretas.

Quando tivermos esse trabalho, iniciaremos o servidor apenas para garantir que tudo esteja bem.

 $ iex -S mix phoenix.server 

Agora devemos visitar http: // localhost: 4000 / e veja a página "Bem-vindo à Phoenix!". Agora que temos uma boa linha de base, vamos adicionar nosso andaime básico para a criação de postagens, uma vez que este é, afinal, um mecanismo de blogs.

A primeira coisa que faremos é utilizar um dos geradores de Phoenix para construir-nos não só o modelo e a migração do Ecto, mas o andaime da UI para lidar com as operações CRUD ( Criar, Ler, Atualizar, Excluir ) para o nosso objeto Post. Uma vez que este é um motor de blog muito, muito simples agora, vamos ficar com um título e um corpo; o título será uma string e o corpo será texto. A sintaxe para esses geradores é bastante direta:

mix phoenix.gen.html [Nome do modelo] [Nome da tabela] [Nome da coluna: tipo de coluna] …

 $ mix phoenix.gen.html Post posts título: string body: text 

Saída:

 * criando web / controllers / post_controller.ex 
...
 Adicione o recurso ao escopo do seu navegador em web / router.ex: 
 recursos "/ posts", PostController 
 Lembre-se de atualizar seu repositório executando migrações: 
 $ mix ecto.migrate 

Em seguida, para tornar este andaime acessível (e parar o Elixir de reclamar), vamos abrir o web / router.ex e adicionar o seguinte ao nosso escopo da raiz (o escopo "/", usando o canal do navegador ):

 recursos "/ posts", PostController 

Finalmente, vamos nos certificar de que nosso banco de dados tenha esta nova migração carregada chamando mix ecto.migrate .

Saída:

 Compilando 9 arquivos (.ex) 
Aplicativo pxblog gerado
 15: 52: 20.004 [info] == executando Pxblog.Repo.Migrations.CreatePost.change / 0 forward 
 15: 52: 20.004 [info] criar postagens da mesa 
 15: 52: 20.019 [info] == Migrado em 0.0s 

Finalmente, vamos reiniciar nosso servidor e, em seguida, visite http: // localhost: 4000 / posts e devemos ver o cabeçalho "Listagem de postagens" com uma tabela contendo as colunas em nosso objeto.

Mexa em torno de um pouco e você deve ser capaz de criar novas postagens, editar postagens e excluir postagens. Muito legal para muito pouco trabalho!

Passo 1B: Testes escritos para postagens

A coisa bonita de trabalhar com os andaimes no início é que ele criará os testes de linha de base para nós desde o início. Nós realmente não precisamos modificar muito ainda, uma vez que não mudamos realmente nenhum dos andaimes, mas vamos analisar o que foi criado para nós, para que possamos estar melhor preparados para escrever nossos próprios testes mais tarde.

Primeiro, abriremos o teste / models / post_test.exs e dê uma olhada:

 defmodule Pxblog.PostTest do 
use Pxblog.ModelCase
 alias Pxblog.Post 
 @valid_attrs% {body: "some content", título: "some content"} 
@invalid_attrs% {}
 teste "conjunto de alterações com atributos válidos" faça 
changeset = Post.changeset (% Post {}, @valid_attrs)
afirmar changeset.valid?
fim
 teste "conjunto de alterações com atributos inválidos" faça 
changeset = Post.changeset (% Post {}, @invalid_attrs)
refutar changeset.valid?
fim
fim

Vamos separar isso e entender o que está acontecendo.

defmodule Pxblog.PostTest do

Claramente, precisamos definir o nosso módulo de teste usando o espaço para nome da nossa aplicação.

use Pxblog.ModelCase

Em seguida, contamos para este módulo que ele estará usando as funções e DSL introduzidas pelo conjunto de macro ModelCase.

alias Pxblog.Post

Agora, certifique-se de que o teste tenha visibilidade no próprio modelo.

@valid_attrs% {body: "some content", título: "some content"}

Configure alguns atributos básicos válidos que poderiam criar um conjunto de alterações bem-sucedido. Isso apenas fornece uma variável de nível de módulo que podemos tirar de cada vez que queremos poder criar um modelo válido.

@invalid_attrs% {}

Como o acima, mas criando, sem surpresa, um conjunto de atributos inválidos.

 teste "conjunto de alterações com atributos válidos" faça 
changeset = Post.changeset (% Post {}, @valid_attrs)
afirmar changeset.valid?
fim

Agora, criamos nosso teste, dando-lhe um nome baseado em string usando a função "testar". Dentro do nosso corpo de funções, primeiro criamos um conjunto de mudanças do modelo Post (dado uma estrutura em branco e a lista de parâmetros válidos). Afirmamos que o conjunto de alterações é válido, já que é isso que esperamos com a variável @valid_attrs.

 teste "conjunto de alterações com atributos inválidos" faça 
changeset = Post.changeset (% Post {}, @invalid_attrs)
refutar changeset.valid?
fim

Finalmente, verificamos contra a criação de um conjunto de mudanças com uma lista de parâmetros inválidos e, em vez de confirmar que o conjunto de alterações é válido, realizamos a operação inversa. refutar é essencialmente afirmativo, não é verdade.

Este é um bom exemplo de como escrever um modelo de arquivo de teste. Agora vamos dar uma olhada nos testes do controlador.

Vamos dar uma olhada no topo, uma vez que tudo parece mais ou menos o mesmo:

 defmodule Pxblog.PostControllerTeste faça 
use Pxblog.ConnCase
 alias Pxblog.Post 
@valid_attrs% {body: "some content", título: "some content"}
@invalid_attrs% {}

O primeiro conjunto de alterações pode ver é Pxblog.ConnCase; Estamos confiando no DSL que está exposto para os testes do nível do controlador. Além disso, o resto das linhas deve ser bastante familiar.

Vamos dar uma olhada no primeiro teste:

 teste "lista todas as entradas no índice",% {conn: conn} do 
conn = get conn, post_path (conn,: index)
assert html_response (conn, 200) = ~ "Lista de postagens"
fim

Aqui, agarramos a variável "conn" que será enviada através de um bloco de configuração no ConnCase. Vou explicar isso mais tarde. O próximo passo é para nós chamar o verbo apropriado para atingir a rota esperada, que no nosso caso é uma solicitação de obtenção contra a nossa ação de índice.

Afirmamos então que a resposta dessa ação retorna HTML com um status de 200 ("ok") e contém a frase "Listagem de postagens".

 teste "renderiza formulário para novos recursos",% {conn: conn} do 
conn = get conn, post_path (conn,: novo)
assert html_response (conn, 200) = ~ "Nova postagem"
fim

O próximo teste é basicamente o mesmo, mas estamos apenas validando a ação "nova" em vez disso. Coisas simples.

 teste "cria recursos e redireciona quando os dados são válidos",% {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)
fim

Agora, estamos fazendo algo novo. Primeiro, desta vez, estamos postando para o ajudante post_path com nossa lista de parâmetros válidos. Esperamos que seja redirecionado para a rota de índice para o recurso de publicação. redirecionado_para capturar um objeto de conexão como um argumento, já que precisamos ver para onde esse objeto de conexão foi redirecionado.

Finalmente, afirmamos que o objeto representado por esses parâmetros válidos é inserido no banco de dados com sucesso através da consulta do nosso Ecto Repo, procurando um modelo de Post que corresponda aos nossos parâmetros @valid_attrs.

Agora, queremos testar o caminho negativo para criar um novo Post.

 teste "não cria recurso e processa erros quando os dados são inválidos",% {conn: conn} do 
conn = post conn, post_path (conn,: create), post: @invalid_attrs
assert html_response (conn, 200) = ~ "Nova postagem"
fim

Então, publicamos no mesmo caminho de criação, mas com nossa lista de parâmetros invalid_attrs, e afirmamos que ele faz o formulário New Post novamente.

 teste "mostra recurso escolhido",% {conn: conn} do 
post = Repo.insert! %Postar{}
conn = get conn, post_path (conn,: show, post)
assert html_response (conn, 200) = ~ "Mostrar postagem"
fim

Para testar a nossa ação de exibição, nós nos certificamos de que criamos um modelo de Post para trabalhar. Em seguida, chamamos nossa função get para o ajudante post_path, e nós garantimos que ele retorna o recurso apropriado. No entanto, se tentarmos pegar um caminho de exibição para um recurso que não existe, fazemos o seguinte:

 teste "torna a página não encontrada quando id é inexistente",% {conn: conn} do 
assert_error_sent 404, fn ->
get conn, post_path (conn,: show, -1)
fim
fim

Nós vemos um novo padrão aqui, mas que é bastante simples de digerir. Esperamos que, se buscarmos um recurso que não existe, devemos receber um 404. Depois, passamos uma função anônima que contém o código que queremos executar, que deve retornar esse erro. Simples!

O resto dos testes são apenas repetições do acima para cada caminho, com exceção da nossa ação de exclusão. Vamos dar uma olhada:

 teste "exclui o recurso escolhido",% {conn: conn} do 
post = Repo.insert! %Postar{}
conn = delete conn, post_path (conn,: delete, post)
assert redirected_to (conn) == post_path (conn,: index)
refutar Repo.get (Post, post.id)
fim

Aqui, a maioria do que vemos é a mesma, com exceção de usar nosso verbo de exclusão. Afirmamos que redirecionamos para fora da página de exclusão de volta ao índice, mas fazemos algo novo aqui: refutamos que o objeto Post existe mais. Assert e Refute são verdadeiros, de modo que a existência de um objeto em tudo funcionará com um Assert e fará com que um Refute falhe.

Nós não adicionamos nenhum código à nossa visão, então não fazemos nada com nosso módulo PostView.

Etapa 2: Adicionando usuários

Seguiremos exatamente os mesmos passos que seguimos com a criação do nosso objeto Post para criar nosso objeto Usuário, exceto com algumas colunas diferentes. Primeiro, vamos correr:

 $ mix phoenix.gen.html Usuário usuário usuário: string email: string password_digest: string 

Saída:

 * criando web / controllers / user_controller.ex 
...
 Adicione o recurso ao escopo do seu navegador em web / router.ex: 
 Recursos "/ usuários", UserController 
 Lembre-se de atualizar seu repositório executando migrações: 
 $ mix ecto.migrate 

Em seguida, abriremos web / router.ex e adicione o seguinte ao escopo do nosso navegador novamente:

 Recursos "/ usuários", UserController 

A sintaxe aqui está definindo uma rota confiável padrão onde o primeiro argumento é o URL e o segundo é o nome da classe do controlador. Em seguida, executaremos mix ecto.migrate

Saída:

 Compilando 11 arquivos (.ex) 
Aplicativo pxblog gerado
 16: 02: 03.987 [info] == executando Pxblog.Repo.Migrations.CreateUser.change / 0 forward 
 16: 02: 03.987 [info] criar usuários de mesa 
 16: 02: 03.996 [info] == Migrado em 0.0s 

E, finalmente, reinicie o servidor e confira http: // localhost: 4000 / users. Podemos agora criar Posts e Usuários independentemente! Infelizmente, este não é um blog muito útil ainda. Afinal, podemos criar usuários (qualquer pessoa pode, na verdade), mas não podemos nem fazer login. Além disso, as digestões de senha não são provenientes de nenhum algoritmo de criptografia; O usuário está apenas criando-os e estamos armazenando-os em texto simples! Não é bom!

Vamos fazer isso parecer mais uma tela de registro de usuário real.

Nossos testes para as coisas do usuário são exatamente iguais ao que foi gerado automaticamente para nossas postagens, então deixamos aqueles sozinhos até começar a modificar a lógica (IE, agora mesmo!)

Etapa 3: salvar um Hash de senha em vez de uma senha

Quando visitamos / users / new , vemos três campos: Nome de usuário , Email e PasswordDigest . Mas quando você se cadastrou em outros sites, você entraria uma senha e uma senha de confirmação! Como podemos corrigir isso?

Em web / templates / user / form.html.eex , exclua as seguintes linhas:

 <div class = "form-group"> 
<% = label f,: password_digest, classe: "controle-etiqueta"%>
<% = text_input f,: password_digest, class: "form-control"%>
<% = error_tag f,: password_digest%>
</ div>

E adicione em seu lugar:

 <div class = "form-group"> 
<% = label f,: senha, "Senha", classe: "controle-etiqueta"%>
<% = password_input f,: senha, classe: "form-control"%>
<% = error_tag f,: senha%>
</ div>

<div class = "form-group">
<% = label f,: password_confirmation, "Confirmação de senha", classe: "controle-etiqueta"%>
<% = password_input f,: password_confirmation, classe: "form-control"%>
<% = error_tag f,: password_confirmation%>
</ div>

Atualize a página (deve acontecer automaticamente), insira os detalhes do usuário, acesse o envio.

Erro:

 Opa, algo deu errado! Verifique os erros abaixo. 

Isso ocorre porque estamos criando uma senha e confirmação de senha, mas nada está sendo feito para criar a senha real_digest. Vamos escrever algum código para fazer isso. Primeiro, vamos modificar o esquema atual para fazer algo novo:

Em web / models / user.ex :

 esquema "usuários" faz 
campo: nome de usuário,: ??string
campo: email:: string
campo: password_digest: string
 timestamps 
 # Campos virtuais 
campo: senha:: string, virtual: true
campo: password_confirmation,: string, virtual: true
fim

Observe a adição dos dois campos,: senha e: senha_confirmação. Estamos declarando estes como campos virtuais , pois estes na verdade não existem em nosso banco de dados, mas precisam existir como propriedades em nossa estrutura do usuário. Isso também nos permite aplicar transformações em nossa função de conjunto de alterações.

Em seguida, modificamos a lista de campos obrigatórios e campos casted para incluir: senha e: password_confirmation .

 def changeset (struct, params \% {}) do 
struct
|> moldar (params, [: nome de usuário,: ??email,: senha,: senha_confirmação])
|> validate_required ([: nome de usuário,: ??email,: senha,: password_confirmation])
fim

Se você executar test / models / user_test.exs neste momento, você notará que nosso teste de "changeset com atributos válidos" está falhando. Isso porque fizemos uma senha e senha_confirmação necessárias, mas não atualizamos nosso mapa @valid_attrs para incluir também. Vamos mudar essa linha para:

 @valid_attrs% {email: " test@test.com ", senha: "test1234", password_confirmation: "test1234", nome de usuário: "testuser"} 

Nossos testes de modelo devem voltar a passar! Também precisamos que nossos testes de controle passem. Em teste / controladores / user_controller_test.exs , faremos algumas modificações. Primeiro, distinguiremos os atributos de criação válidos e atributos de pesquisa válidos.

 @valid_create_attrs% {email: " test@test.com ", senha: "test1234", password_confirmation: "test1234", nome de usuário: "testuser"} 
@valid_attrs% {email: " test@test.com ", nome de usuário: "testuser"}

Então, modificaremos nosso teste de criação:

 teste "cria recursos e redireciona quando os dados são válidos",% {conn: conn} do 
conn = post conn, user_path (conn,: create), usuário: @valid_create_attrs
Assert redirected_to (conn) == user_path (conn,: index)
afirmar Repo.get_by (Usuário, @valid_attrs)
fim

E nosso teste de atualização:

 teste "atualiza o recurso escolhido e redireciona quando os dados são válidos",% {conn: conn} do 
usuário = Repo.insert! %Do utilizador{}
conn = put conn, user_path (conn,: update, user), usuário: @valid_create_attrs
Assert redirected_to (conn) == user_path (conn,: show, user)
afirmar Repo.get_by (Usuário, @valid_attrs)
fim

Com os nossos testes de volta ao verde, precisamos modificar a função changeset para alterar nossa senha em uma digestão de senha em tempo real:

 def changeset (struct, params \% {}) do 
struct
|> moldar (params, [: nome de usuário,: ??email,: senha,: senha_confirmação])
|> validate_required ([: nome de usuário,: ??email,: senha,: password_confirmation])
|> hash_password
fim
 defp hash_password (changeset) fazer 
conjunto de mudanças
|> put_change (: password_digest, "ABCDE")
fim

Neste momento, estamos apenas ignorando o comportamento de nossa função de hashing. O primeiro passo é garantir que possamos modificar o nosso conjunto de mudanças à medida que avançamos. Vamos verificar esse comportamento primeiro. Volte para http: // localhost: 4000 / usuários em nosso navegador, clique em "Novo usuário" e crie um novo usuário com todos os detalhes. Quando atingimos a página de índice novamente, devemos esperar para ver o usuário criado com um valor de password_digest de "ABCDE".

E execute nossos testes novamente para este arquivo. Nossos testes estão passando, mas não adicionamos nenhum teste para este novo trabalho hash_password . Vamos adicionar um teste em nosso conjunto de testes modelo que irá adicionar um teste no digest da senha:

 teste "password_digest value is set to hash" do 
changeset = User.changeset (% User {}, @valid_attrs)
assert get_change (changeset,: password_digest) == "ABCDE"
fim

Este é um excelente passo a frente, mas não é fantástico para a segurança! Vamos modificar nossos hashes para ser um hashes de senha real com o Bcrypt, cortesia da biblioteca de Comeonin .

Primeiro, abra mix.exs e adicione : comeonin à nossa lista de aplicativos:

 aplicativo def 
[mod: {Pxblog, []},
Aplicações: [: phoenix,: phoenix_pubub,: phoenix_html,: cowboy,: logger,: gettext,
: phoenix_ecto,: postgrex,: comeonin]]
fim

E também precisaremos modificar nossa definição de deps:

 defp deps do 
[{: phoenix, "~> 1.2.0"},
{: phoenix_pubub, "~> 1,0"},
{: phoenix_ecto, "~> 3.0"},
{: postgrex, "> = 0.0.0"},
{: phoenix_html, "~> 2.6"},
{: phoenix_live_reload, "~> 1.0", apenas:: dev},
{: gettext, "~> 0.11"},
{: cowboy, "~> 1.0"},
{: comeonin, "~> 2.3"}]
fim

Mesmo aqui, observe a adição de {: comeonin, "~> 2.3"} . Agora, vamos desligar o servidor que estamos executando e executar mix deps.get . Se tudo correr bem (deve!), Então você deve ser capaz de reiniciar iex -S mix phoenix.server para reiniciar seu servidor.

Nosso método antigo de hash_password é limpo, mas precisamos dele para realmente ter nossa senha. Uma vez que adicionamos a biblioteca comeonin , que nos fornece um bom módulo Bcrypt com um método hashpwsalt, então vamos importar isso para o nosso modelo de usuário.

Em web / models / user.ex , adicione a seguinte linha ao topo logo abaixo do nosso uso Pxblog.Web,: linha do modelo :

 importa Comeonin.Bcrypt, apenas: [hashpwsalt: 1] 

O que estamos fazendo aqui é puxar o módulo Bcrypt no espaço de nome Comeonin e importar o método hashpwsalt com uma aridade de 1. E agora vamos modificar nosso método hash_password para funcionar.

 defp hash_password (changeset) fazer 
se password = get_change (changeset,: password) do
conjunto de mudanças
|> put_change (: password_digest, hashpwsalt (senha))
outro
conjunto de mudanças
fim
fim

Vamos tentar criar um usuário novamente! Desta vez, depois de entrar nos nossos dados para o nome de usuário, e-mail, senha e confirmação de senha, devemos ver um resumo criptografado exibido no campo password_digest !

Agora, vamos querer trabalhar na função hash_password que adicionamos. A primeira coisa que queremos fazer é alterar a configuração do nosso ambiente de teste para garantir que nossos testes não diminuam drasticamente ao trabalhar com a criptografia de senha. Abra config / test.exs e adicione o seguinte na parte inferior:

 config: comeonin, bcrypt_log_rounds: 4 

Isso irá configurar o ComeOnIn quando estiver em nosso ambiente de teste para não tentar muito criptografar nossa senha. Uma vez que isso é apenas para testes, não precisamos de nada super seguro e preferiria sanidade e velocidade em vez disso! Em config / prod.exs , queremos mudar isso para:

 config: comeonin, bcrypt_log_rounds: 14 

Vamos escrever uma prova para a chamada de Comeonin. Vamos torná-lo um pouco menos específico; Nós só queremos verificar a encriptação. Em teste / models / user_test.exs :

 teste "password_digest value is set to hash" do 
changeset = User.changeset (% User {}, @valid_attrs)
assert Comeonin.Bcrypt.checkpw (@ valid_attrs.password, Ecto.Changeset.get_change (changeset,: password_digest))
fim

Para mais alguma cobertura de teste, vamos adicionar um caso para lidar se a senha = linha get_change () não for atingida:

 teste "password_digest value is not set set if password is nil" do 
changeset = User.changeset (% User {},% {email: "test@test.com", senha: nil, password_confirmation: nil, nome de usuário: "test"})
refute Ecto.Changeset.get_change (changeset,: password_digest)
fim

Uma vez que assert / refute use truthiness, podemos ver se esse bloco de código deixa o password_digest em branco, o que faz! Estamos a fazer um bom trabalho para cobrir o nosso trabalho com especificações!

Passo 4: Inicie sessão!

Vamos adicionar um novo controlador, SessionController e uma visão acompanhante, SessionView. Começaremos simples e construiremos o nosso caminho até uma melhor implementação ao longo do tempo.

Criar web / controllers / session_controller.ex:

 defmodule Pxblog.SessionController do 
use Pxblog.Web,: controlador
 def new (conn, _params) do 
render conn, "new.html"
fim
fim

Crie web / views / session_view.ex :

 defmodule Pxblog.SessionView do 
use Pxblog.Web,: veja
fim

Crie web / templates / session / new.html.eex :

 <h2> Login </ h2> 

E, finalmente, vamos atualizar o roteador para incluir esse novo controlador. Adicione a seguinte linha ao nosso escopo "/":

 recursos "/ sessões", SessionController, apenas: [: novo] 

A única rota que queremos expor por enquanto é nova , então vamos limitar isso apenas para isso. Mais uma vez, queremos manter as coisas simples e construir a partir de uma base estável.

Agora visitemos http: // localhost: 4000 / sessions / new e devemos esperar para ver o cabeçalho da estrutura Phoenix e o cabeçalho "Login".

Vamos dar uma forma real. Criar web / templates / session / form.html.eex :

 <% = form_for @changeset, @action, fn f ->%> 
<% = if f.errors! = [] do%>
<div class = "alert alert-danger">
<p> Ops, algo deu errado Verifique os erros abaixo: </ p>
<ul>
<% = for {attr, message} <- f.errors do%>
<li> <% = humanize (attr)%> <% = message%> </ li>
<% end%>
</ ul>
</ div>
<% end%>
 <div class = "form-group"> 
<label> Nome de usuário </ label>
<% = text_input f,: nome de usuário, classe: "form-control"%>
</ div>
 <div class = "form-group"> 
<label> Senha </ label>
<% = password_input f,: senha, classe: "form-control"%>
</ div>
 <div class = "form-group"> 
<% = enviar "Enviar", classe: "btn btn-primary"%>
</ div>
<% end%>

E modifique web / templates / session / new.html.eex para chamar nosso novo formulário adicionando uma linha:

 <% = renderizar "form.html", changeset: @changeset, action: session_path (@conn,: create)%> 

O autoreloading acabará exibindo uma página de erro agora porque não definimos o @changeset , o que, como você pode imaginar, precisa ser um conjunto de alterações. Uma vez que estamos trabalhando com o objeto membro, que tem campos de nome de usuário e senha já existentes, vamos usar isso!

Em web / controllers / session_controller.ex , precisamos alias do modelo do usuário para poder usá-lo ainda mais. No topo da nossa classe, sob o nosso uso Pxblog.Web,: linha do controlador , adicione o seguinte:

 alias Pxblog.User 

E na nova função, modifique a chamada para renderizar da seguinte maneira:

 render conn, "new.html", changeset: User.changeset (% User {}) 

Nós precisamos passar a conexão, o modelo que estamos processando (menos a eex) e uma lista de variáveis ??adicionais que devem ser expostas aos nossos modelos. Neste caso, queremos expor @changeset, então especificamos changeset: aqui, e nós damos o conjunto de alterações Ecto para o Usuário com uma estrutura de Usuário em branco. (% User {} é um User Struct sem valores definidos)

Atualize agora e obtemos uma mensagem de erro diferente que deve se parecer com o seguinte:

 Nenhuma cláusula de ajuda para Pxblog.Router.Helpers.session_path / 2 definida para ação: criar. 
As seguintes ações session_path são definidas em seu roteador:
*:Novo

Em nossa forma, estamos referenciando uma rota que ainda não existe! Estamos usando o ajudante de sessão_path, passando o objeto @conn, mas depois especificando: crie o caminho que ainda não criamos.

Nós ficamos parte do caminho. Agora vamos fazer isso para que possamos publicar nossos detalhes de login e configurar a sessão.

Vamos atualizar nossas rotas para permitir a publicação para criar.

Em web / router.ex , altere nossa referência para SessionController para incluir também: create.

 recursos "/ sessões", SessionController, apenas: [: novo,: criar] 

Em web / controllers / session_controller.ex , precisamos importar uma nova função, checkpw do módulo Bcrypt da Comeonin. Fazemos isso através da seguinte linha:

 importa Comeonin.Bcrypt, apenas: [checkpw: 2] 

Esta linha está dizendo "Importar do módulo Comeonin.Bcrypt, mas apenas o checkpw funciona com uma aridade de 2". E então vamos adicionar um plug scrub_params para lidar com dados do usuário. Antes de nossas funções, adicionaremos:

 plug: scrub_params, "usuário" quando a ação em [: create] 

"Scrub_params" é uma função especial que limpa qualquer entrada do usuário; no caso em que algo seja transmitido como uma string em branco, por exemplo, scrub_params o converterá em um valor nulo para evitar criar entradas em seu banco de dados que tenham cadeias vazias.

E vamos adicionar nossa função para lidar com a publicação criativa. Vamos adicionar isso ao fundo do nosso módulo SessionController. Haverá um monte de código aqui, então vamos levá-lo peça por peça.

Em web / controllers / session_controller.ex :

 def create (conn,% {"user" => user_params}) fazer 
Repo.get_by (Usuário, nome de usuário: user_params ["nome de usuário"])
|> sign_in (user_params ["senha"], conn)
fim

O primeiro bit deste código, Repo.get_by (Usuário, nome de usuário: user_params ["nome de usuário"]) puxa o primeiro usuário aplicável do nosso Ecto Repo que possui um nome de usuário correspondente ou, de outra forma, retornará nulo.

Aqui está uma saída para verificar esse comportamento:

 iex (3)> Repo.get_by (Usuário, nome de usuário: "flibbity") 
[debug] SELECT u0. "id", u0. "username", u0. "email", u0. "password_digest", u0. "insert_at", u0. "updated_at" FROM "usuários" AS u0 WHERE (u0. " nome de usuário "= $ 1) [" flibbity "] consulta OK = 0.7ms
nada
 iex (4)> Repo.get_by (Usuário, nome de usuário: "teste") 
[debug] SELECT u0. "id", u0. "username", u0. "email", u0. "password_digest", u0. "insert_at", u0. "updated_at" FROM "usuários" AS u0 WHERE (u0. " nome de usuário "= $ 1) [" teste "] consulta OK = 0.8ms
% Pxblog.User {__ meta__:% Ecto.Schema.Metadata {source: "users", state:: loaded},
email: "teste", id: 15,
insert_at:% Ecto.DateTime {dia: 24, hora: 19, min: 6, mês: 6, seg: 14,
usec: 0, ano: 2015}, senha: nil, password_confirmation: nil,
password_digest: "$ 2b $ 12 $ RRkTZiUoPVuIHMCJd7yZUOnAptSFyM9Hw3Aa88ik4erEsXTZQmwu2",
updated_at:% Ecto.DateTime {dia: 24, hora: 19, min: 6, mês: 6, seg: 14,
usec: 0, ano: 2015}, nome de usuário: "teste"}

Em seguida, levamos o usuário e encadear esse usuário em um método de assinatura. Ainda não escrevemos isso, então vamos fazer isso!

 defp sign_in (user, password, conn) quando is_nil (user) do 
con
|> put_flash (: erro, "combinação de nome de usuário / senha inválida!")
|> redirecionar (para: page_path (conn,: index))
fim
 defp sign_in (user, password, conn) do 
se checkpw (senha, user.password_digest) fazer
con
|> put_session (: current_user,% {id: user.id, nome de usuário: user.username})
|> put_flash (: info, "Iniciar sessão com sucesso!")
|> redirecionar (para: page_path (conn,: index))
outro
con
|> put_session (: current_user, nil)
|> put_flash (: erro, "combinação de nome de usuário / senha inválida!")
|> redirecionar (para: page_path (conn,: index))
fim
fim

A primeira coisa a notar é a ordem em que esses métodos são definidos. O primeiro desses métodos tem uma cláusula de guarda anexada a ele, de modo que o método só será executado quando essa cláusula de proteção for verdadeira, então, se o usuário for nulo, nós redirecione novamente para o índice do caminho da página (raiz) com uma mensagem flash apropriada.

O segundo método será chamado se a cláusula de guarda for falsa e irá lidar com todos os outros cenários. Verificamos o resultado dessa função checkpw e, se for verdade, definimos o usuário na variável de sessão current_user e redirecionamos com uma mensagem de sucesso. Caso contrário, limpar a sessão de usuário atual, definir uma mensagem de erro e redirecionar de volta para a raiz.

Se retornarmos à nossa página de login http: // localhost: 4000 / sessions / new, devemos testar o login com um conjunto válido de credenciais e credenciais inválidas e ver as mensagens de erro apropriadas!

Precisamos também escrever algumas especificações para este controlador. Criaremos test / controllers / session_controller_test.exs e preenchê-lo com o seguinte:

 defmodule Pxblog.SessionControllerTeste faça 
use Pxblog.ConnCase
alias Pxblog.User
 configurar fazer 
User.changeset (% User {},% {nome de usuário: "test", senha: "test", password_confirmation: "test", email: " test@test.com "})
| Repo.insert
{: ok, conn: build_conn ()}
fim
 teste "mostra o formulário de login",% {conn: conn} do 
conn = get conn, session_path (conn,: new)
assert html_response (conn, 200) = ~ "Login"
fim
 teste "cria uma nova sessão de usuário para um usuário válido",% {conn: conn} do 
conn = post conn, session_path (conn,: create), usuário:% {username: "test", password: "test"}
assert get_session (conn,: current_user)
assert get_flash (conn,: info) == "Inscreva-se com sucesso!"
assert redirected_to (conn) == page_path (conn,: index)
fim
 teste "não cria uma sessão com um login incorreto",% {conn: conn} do 
conn = post conn, session_path (conn,: create), usuário:% {username: "test", password: "wrong"}
refutar get_session (conn,: current_user)
assert get_flash (conn,: error) == "Combinação de nome de usuário / senha inválida!"
assert redirected_to (conn) == page_path (conn,: index)
fim
 teste "não cria uma sessão se o usuário não existir",% {conn: conn} do 
conn = post conn, session_path (conn,: create), usuário:% {username: "foo", senha: "wrong"}
assert get_flash (conn,: error) == "Combinação de nome de usuário / senha inválida!"
assert redirected_to (conn) == page_path (conn,: index)
fim
fim

Começamos com o nosso bloco de configuração padrão e escrevemos uma asserção bastante padrão para um pedido de obtenção. Os bits de criação são onde isso começa a ficar mais interessante:

 teste "cria uma nova sessão de usuário para um usuário válido",% {conn: conn} do 
conn = post conn, session_path (conn,: create), usuário:% {username: "test", password: "test"}
assert get_session (conn,: current_user)
assert get_flash (conn,: info) == "Inscreva-se com sucesso!"
assert redirected_to (conn) == page_path (conn,: index)
fim

O primeiro bit aqui é publicar no nosso caminho de criação de sessão. Em seguida, verificamos que definimos a variável de sessão current_user, a mensagem flash para a ação e, finalmente, afirmamos onde estamos sendo redirecionados.

As outras duas chamadas estão apenas usando o mesmo tipo de asserções (e em um caso, uma refutação) para garantir que estamos testando todos os caminhos que a função sign_in pode atingir. Novamente, coisas muito simples!

Passo 5: expor o nosso actual usuário

Vamos modificar nosso layout para exibir uma mensagem ou um link, dependendo se o membro estiver logado ou não.

Na web / views / layout_view.ex , vamos escrever um auxiliar que facilite a nossa acessibilidade às informações do usuário, então adicionaremos:

 def current_user (conn) do 
Plug.Conn.get_session (conn,: current_user)
fim

Vamos escrever uma prova para garantir que isso funcione.

Em web / templates / layout / app.html.eex , em vez do link "Começar", vamos fazer o seguinte:

 <li> 
<% = if user = current_user (@conn) do%>
logado como
<strong> <% = user.username%> </ strong>
<br>
<% = link "Log out", para: session_path (@conn,: delete, user.id), método:: delete%>
<% else%>
<% = link "Log in", para: session_path (@conn,: new)%>
<% end%>
</ li>

Novamente, vamos passar por essa peça por peça. Uma das primeiras coisas que precisamos fazer é descobrir quem é o usuário atual, assumindo que eles estão logados. Nós estamos indo com uma abordagem de primeiro e primeiro refactor, então, por agora, vamos definir apenas um objeto de usuário da sessão diretamente no nosso modelo. get_session faz parte do objeto Plug.Conn. Se o usuário existe (isso tira proveito dos valores de verdade de Rubix em Elixir, na medida em que nula retornará falso aqui.)

Se o usuário estiver logado, também queremos fornecer um link de logout também. Embora isso ainda não exista, ele acabará por existir, então, no momento, agora vamos enviá-lo. Nós tratamos uma sessão como um recurso, então, para sair, vamos "excluir" a sessão, então vamos fornecer um link aqui.

Nós também queremos enviar o nome de usuário do usuário atual. Estamos armazenando a estrutura do usuário na variável de sessão current_user, para que possamos acessar o nome de usuário como user.username.

Se não conseguimos encontrar o usuário, então forneceremos o link de login. Novamente, estamos tratando sessões como um recurso aqui, então "novo" fornecerá a rota apropriada para criar uma nova sessão.

Você provavelmente percebeu que, quando tudo atualizado, estamos recebendo outra mensagem de erro sobre uma cláusula de função correspondente faltando. Vamos adicionar nossa rota de exclusão também para manter Phoenix feliz!

Em web / router.ex , modificaremos nossa rota de "sessões" para permitir também: excluir:

 recursos "/ sessões", SessionController, apenas: [: novo,: create,: delete] 

E vamos modificar o controlador também. Em web / controllers / session_controller.ex , adicione o seguinte:

 def delete (conn, _params) do 
con
|> delete_session (: current_user)
|> put_flash (: info, "Assinado com sucesso!")
|> redirecionar (para: page_path (conn,: index))
fim

Uma vez que acabamos de excluir a chave: current_user, na verdade, não nos importa o que são os params, por isso marcamos aqueles como não utilizados com um sublinhado. Definimos uma mensagem flash para tornar a UI um pouco mais clara para o usuário e redirecionar de volta para nossa rota de raiz.

Agora podemos fazer login, sair e ver as falhas de login também! As coisas estão se formando para o melhor! Mas primeiro, precisamos escrever alguns testes. Começaremos com os testes para o nosso LayoutView.

 defmodule Pxblog.LayoutViewTest do 
use Pxblog.ConnCase, async: true
 alias Pxblog.LayoutView 
alias Pxblog.User
 configurar fazer 
User.changeset (% User {},% {nome de usuário: "test", senha: "test", password_confirmation: "test", email: " test@test.com "})
| Repo.insert
{: ok, conn: build_conn ()}
fim
 teste "o usuário atual retorna o usuário na sessão",% {conn: conn} do 
conn = post conn, session_path (conn,: create), usuário:% {username: "test", password: "test"}
afirmar LayoutView.current_user (conn)
fim
 teste "o usuário atual não retorna nada se não houver usuário na sessão",% {conn: conn} do 
usuário = Repo.get_by (Usuário,% {nome de usuário: "teste"})
conn = delete conn, session_path (conn,: delete, user)
refutar LayoutView.current_user (conn)
fim
fim

Vamos analisar. A primeira coisa que vamos fazer é alias dos módulos LayoutView e User para que possamos encurtar alguns dos nossos códigos. Em seguida, no nosso bloco de configuração, criamos um Usuário e o lançamos no banco de dados. Em seguida, devolvemos o nosso padrão {: ok, conn: build_conn ()} tuple.

Em seguida, escrevemos o nosso primeiro teste, iniciando sessão na nossa sessão, criamos uma ação e afirmamos que, depois de efetuar o login, a função LayoutView.current_user retorna alguns dados.

Em seguida, escrevemos para o nosso caso negativo; excluímos explicitamente a sessão e refutamos que um usuário seja retornado da nossa chamada de usuário atual. Também atualizamos nosso SessionController adicionando uma ação de exclusão, então precisamos fazer todos os testes.

 teste "exclui a sessão do usuário",% {conn: conn} do 
usuário = Repo.get_by (Usuário,% {nome de usuário: "teste"})
conn = delete conn, session_path (conn,: delete, user)
refutar get_session (conn,: current_user)
assert get_flash (conn,: info) == "Assinado com sucesso!"
assert redirected_to (conn) == page_path (conn,: index)
fim

Certifique-se de que current_user da sessão esteja em branco, então verificamos a mensagem flash e certifique-se de que sejam redirecionados para fora!

Possíveis erros com compilação de ativos

Uma coisa a notar é que você pode acabar batendo um erro quando você está tentando compilar os recursos com brunch. A mensagem de erro que recebi foi:

 16 Dez 23:30:20 - erro: a compilação de 'web / static / js / app.js' falhou. Não foi possível encontrar a predefinição "es2015" em relação ao diretório "web / static / js"; A compilação de 'web / static / js / socket.js' falhou. Não foi possível encontrar a predefinição "es2015" em relação ao diretório "web / static / js"; A compilação de 'deps / phoenix / web / static / js / phoenix.js' falhou. Não foi possível encontrar a predefinição "es2015" em relação ao diretório "deps / phoenix / web / static / js"; A compilação de 'deps / phoenix_html / web / static / js / phoenix_html.js' falhou. Não foi possível encontrar a predefinição "es2015" em relação ao diretório "deps / phoenix_html / web / static / js" 

Você pode corrigir isso com a instalação do babel-preset-es2015 com o NPM. Executei o seguinte comando:

 npm instalar -g babel-preset-es2015 

Agora, se você iniciar o servidor, você deve ver todos os recursos corretamente compilados!

Próximo post nesta série

Escrevendo um Blog Engine em Phoenix e Elixir: Parte 2, Autorização
Atualizado em: 21/07/2016 medium.com

Hacker Noon é como os hackers começam suas tardes. Somos uma parte da família @AMI . Agora estamos aceitando envios e estamos felizes em discutir oportunidades de propaganda e patrocínio .

Para saber mais, leia nossa sobre a página , como / nos envie no Facebook , ou simplesmente, tweet / DM @HackerNoon.

Se você gostou desta história, recomendamos ler nossas últimas histórias de tecnologia e histórias de tecnologia de tendências . Até a próxima, não concorde com as realidades do mundo!