Escribir un motor de blog en Phoenix y Elixir: Parte 1

Última actualización : 20/07/2016

Versiones actuales:

Al momento de escribir esto, las versiones actuales de nuestras aplicaciones son:

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

Si estás leyendo esto y estos no son los últimos, házmelo saber y actualizaré este tutorial en consecuencia.

Instalando Phoenix

Las mejores instrucciones para instalar Phoenix se pueden encontrar en el sitio web de Phoenix.

Paso 1: agreguemos publicaciones

Necesitamos comenzar utilizando la tarea de combinación de Phoenix para crear un nuevo proyecto, que llamaremos "pxblog". Hacemos esto usando la mezcla phoenix.new [project] [command]. Responda S a todas las preguntas, ya que queremos usar phoenix.js y los demás requisitos de front-end.

 mezclar phoenix.new pxblog 

Salida:

 * creando pxblog / config / config.exs 
...
 Obtener e instalar dependencias? [Yn] y 
* ejecuta mix deps.get
* ejecutando npm install && node node_modules / brunch / bin / brunch build
 ¡Estamos todos listos! Ejecute su aplicación Phoenix: 
 $ cd pxblog 
$ mix phoenix.server
 También puede ejecutar su aplicación dentro de IEx (Elixir interactivo) como: 
 $ iex -S mix phoenix.server 
 Antes de continuar, configure su base de datos en config / dev.exs y ejecute: 
 $ mix ecto.create 

Deberíamos ver un montón de resultados que indican que nuestro proyecto se ha completado y que el trabajo inicial ya está hecho.

El paso ecto.create de combinación puede fallar si no hemos configurado correctamente nuestra base de datos postgres o si no hemos configurado nuestra aplicación para usar las credenciales correctas. Si abre config / dev.exs , debería ver algunos detalles de configuración en la parte inferior del archivo:

 # Configure su base de datos 
config: pxblog, Pxblog.Repo,
adaptador: Ecto.Adapters.Postgres,
nombre de usuario: "postgres",
contraseña: "postgres",
base de datos: "pxblog_dev",
nombre de host: "localhost",
pool_size: 10

Simplemente cambie el nombre de usuario y la contraseña a un rol que tenga los permisos de creación de base de datos correctos.

Cuando tengamos eso funcionando, iniciaremos el servidor solo para asegurarnos de que todo esté bien.

 $ iex -S mix phoenix.server 

Ahora deberíamos poder visitar http: // localhost: 4000 / y ver la página "¡Bienvenido a Phoenix!". Ahora que tenemos una buena línea de base, agreguemos nuestro andamio básico para crear publicaciones, ya que, después de todo, se trata de un motor de blogs.

Lo primero que haremos es utilizar uno de los generadores de Phoenix para construir no solo el modelo y la migración de Ecto, sino el andamiaje de UI para manejar las operaciones CRUD ( Crear, Leer, Actualizar, Eliminar ) para nuestro objeto Post. Dado que este es un motor de blog muy, muy simple en este momento, sigamos con un título y un cuerpo; el título será una cadena y el cuerpo será texto. La sintaxis de estos generadores es bastante sencilla:

mix phoenix.gen.html [Nombre del modelo] [Nombre de la tabla] [Nombre de la columna: Tipo de columna] …

 $ mix phoenix.gen.html Título de las publicaciones: cuerpo de la secuencia: texto 

Salida:

 * crear web / controladores / post_controller.ex 
...
 Agregue el recurso al alcance de su navegador en web / router.ex: 
 recursos "/ posts", PostController 
 Recuerde actualizar su repositorio ejecutando migraciones: 
 $ mix ecto.migrate 

Luego, para hacer que este andamio sea accesible (y evitar que Elixir se queje), vamos a abrir web / router.ex, y agregaremos lo siguiente a nuestro alcance raíz (el alcance "/", usando el canal de navegación ):

 recursos "/ posts", PostController 

Finalmente, nos aseguraremos de que nuestra base de datos cargue esta nueva migración llamando a mix ecto.migrate .

Salida:

 Compilando 9 archivos (.ex) 
Aplicación pxblog generada
 15: 52: 20.004 [info] == Ejecutando Pxblog.Repo.Migrations.CreatePost.change / 0 forward 
 15: 52: 20.004 [info] crear publicaciones de mesa 
 15: 52: 20.019 [info] == Migrado en 0.0s 

Finalmente, reiniciemos nuestro servidor, y luego visite http: // localhost: 4000 / posts y deberíamos ver el encabezado "Listing posts" con una tabla que contiene las columnas de nuestro objeto.

Mastica un poco y deberías poder crear nuevas publicaciones, editar publicaciones y eliminar publicaciones. ¡Genial para muy poco trabajo!

Paso 1B: Pruebas de escrituras para publicaciones

Lo bello de trabajar con los andamios desde el principio es que creará las pruebas iniciales para nosotros desde el principio. Ni siquiera tenemos que modificar mucho todavía, ya que no hemos cambiado ninguno de los andamios, pero analicemos lo que se creó para nosotros para que podamos estar mejor preparados para escribir nuestras propias pruebas más adelante.

Primero, abriremos test / models / post_test.exs y lo examinaremos:

 defmodule Pxblog.PostTest do 
usa Pxblog.ModelCase
 alias Pxblog.Post 
 @valid_attrs% {body: "algún contenido", título: "algún contenido"} 
@invalid_attrs% {}
 prueba "changeset con atributos válidos" do 
changeset = Post.changeset (% Post {}, @valid_attrs)
afirmar changeset.valid?
fin
 prueba "changeset with invalid attributes" do 
changeset = Post.changeset (% Post {}, @invalid_attrs)
refute changeset.valid?
fin
fin

Desmontémonos y comprendamos qué está pasando.

defmodule Pxblog.PostTest do

Claramente, necesitamos definir nuestro módulo de prueba usando el espacio de nombres de nuestra aplicación.

usa Pxblog.ModelCase

A continuación, le indicamos a este módulo que va a utilizar las funciones y DSL introducidas por el conjunto de macros ModelCase.

alias Pxblog.Post

Ahora asegúrese de que la prueba tenga visibilidad en el modelo en sí.

@valid_attrs% {body: "algún contenido", título: "algún contenido"}

Configure algunos atributos válidos básicos que podrían crear un conjunto de cambios exitoso. Esto solo proporciona una variable a nivel de módulo que podemos extraer de cada vez que queremos poder crear un modelo válido.

@invalid_attrs% {}

Como el anterior, pero creando, como era de esperar, un conjunto de atributos no válidos.

 prueba "changeset con atributos válidos" do 
changeset = Post.changeset (% Post {}, @valid_attrs)
afirmar changeset.valid?
fin

Ahora, creamos nuestra prueba dándole un nombre basado en cadena utilizando la función "prueba". Dentro de nuestro cuerpo de funciones, primero creamos un conjunto de cambios a partir del modelo de publicación (dado que es una estructura en blanco y la lista de parámetros válidos). Luego afirmamos que el conjunto de cambios es válido, ya que eso es lo que estamos esperando con la variable @valid_attrs.

 prueba "changeset with invalid attributes" do 
changeset = Post.changeset (% Post {}, @invalid_attrs)
refute changeset.valid?
fin

Finalmente, verificamos contra la creación de un conjunto de cambios con una lista de parámetros no válida, y en lugar de afirmar que el conjunto de cambios es válido, realizamos la operación inversa. refutar es esencialmente afirmar que no es verdad.

Este es un buen ejemplo de cómo escribir un archivo de prueba modelo. Ahora echemos un vistazo a las pruebas de controlador.

Echemos un vistazo a la parte superior, ya que todo se ve más o menos lo mismo:

 defmodule Pxblog.PostControllerTest do 
usa Pxblog.ConnCase
 alias Pxblog.Post 
@valid_attrs% {body: "algún contenido", título: "algún contenido"}
@invalid_attrs% {}

El primer conjunto de cambios que se puede ver es Pxblog.ConnCase; confiamos en la DSL que está expuesta para las pruebas de nivel de controlador. Aparte de eso, el resto de las líneas debería ser bastante familiar.

Echemos un vistazo a la primera prueba:

 test "enumera todas las entradas en el índice",% {conn: conn} do 
conn = get conn, post_path (conn,: index)
assert html_response (conn, 200) = ~ "Publicación de publicaciones"
fin

Aquí, tomamos la variable "conn" que se enviará a través de un bloque de configuración en ConnCase. Lo explicaré más tarde. El siguiente paso es que llamemos al verbo apropiado para llegar a la ruta esperada, que en nuestro caso es una solicitud de obtención de nuestra acción de índice.

A continuación, afirmamos que la respuesta de esta acción devuelve HTML con un estado de 200 ("ok") y contiene la frase "Publicaciones de listado".

 prueba "representa formulario para nuevos recursos",% {conn: conn} do 
conn = get conn, post_path (conn,: new)
assert html_response (conn, 200) = ~ "Nueva publicación"
fin

La siguiente prueba es básicamente la misma, pero en su lugar estamos validando la acción "nueva". Cosas simples

 prueba "crea recurso y redirige cuando los datos son 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)
fin

Ahora, estamos haciendo algo nuevo. Primero, esta vez estamos publicando en el auxiliar post_path con nuestra lista de parámetros válidos. Esperamos redirigirnos a la ruta de índice para el recurso de publicación. redirected_to toma un objeto de conexión como argumento, ya que necesitamos ver a dónde fue redirigido ese objeto de conexión.

Finalmente, afirmamos que el objeto representado por esos parámetros válidos se inserta en la base de datos con éxito mediante la consulta de nuestro Ecto Repo, buscando un modelo de publicación que coincida con nuestros parámetros @valid_attrs.

Ahora, queremos probar la ruta negativa para crear una nueva publicación.

 test "no crea recursos y presenta errores cuando los datos no son válidos",% {conn: conn} do 
conn = post conn, post_path (conn,: create), post: @invalid_attrs
assert html_response (conn, 200) = ~ "Nueva publicación"
fin

Por lo tanto, publicamos en la misma ruta de creación pero con nuestra lista de parámetros invalid_attrs, y afirmamos que vuelve a generar el formulario de Nuevo mensaje.

 la prueba "muestra el recurso elegido",% {conn: conn} do 
post = Repo.insert! %Enviar{}
conn = get conn, post_path (conn,: show, post)
assert html_response (conn, 200) = ~ "Mostrar publicación"
fin

Para probar nuestra acción de mostrar, nos aseguramos de crear un modelo de publicación para trabajar. Luego llamamos a nuestra función get al post_path helper y nos aseguramos de que devuelva el recurso apropiado. Sin embargo, si intentamos obtener una ruta de acceso a un recurso que no existe, hacemos lo siguiente:

 test "muestra la página no encontrada cuando id no existe",% {conn: conn} do 
assert_error_sent 404, fn ->
obtener conn, post_path (conn,: mostrar, -1)
fin
fin

Vemos un nuevo patrón aquí, pero que en realidad es bastante simple de digerir. Esperamos que si buscamos un recurso que no existe, recibamos un 404. Luego le pasamos una función anónima que contiene el código que queremos ejecutar que debería devolver ese error. ¡Sencillo!

El resto de las pruebas son solo repeticiones de lo anterior para cada ruta, con la excepción de nuestra acción de eliminación. Vamos a ver:

 la prueba "borra el recurso elegido",% {conn: conn} do 
post = Repo.insert! %Enviar{}
conn = eliminar conn, post_path (conn,: eliminar, publicar)
assert redirected_to (conn) == post_path (conn,: index)
refutar Repo.get (Post, post.id)
fin

Aquí, la mayoría de lo que vemos es lo mismo, con la excepción de usar nuestro verbo de eliminación. Afirmamos que redirigimos desde la página de eliminación al índice, pero hacemos algo nuevo aquí: refutamos que el objeto Post ya existe. Assert y Refute son verdad, por lo que la existencia de un objeto funcionará con un Assert y provocará que un Refute falle.

No agregamos ningún código a nuestra vista, por lo que no hacemos nada con nuestro módulo PostView.

Paso 2: agregar usuarios

Vamos a seguir casi exactamente los mismos pasos que seguimos al crear nuestro objeto Post para crear nuestro objeto Usuario, excepto con algunas columnas diferentes. Primero, correremos:

 $ mix phoenix.gen.html Nombre de usuario de los usuarios: cadena email: cadena password_digest: cadena 

Salida:

 * crear web / controladores / user_controller.ex 
...
 Agregue el recurso al alcance de su navegador en web / router.ex: 
 recursos "/ usuarios", UserController 
 Recuerde actualizar su repositorio ejecutando migraciones: 
 $ mix ecto.migrate 

A continuación, abriremos web / router.ex y agregaremos lo siguiente al alcance de nuestro navegador nuevamente:

 recursos "/ usuarios", UserController 

La sintaxis aquí define una ruta de recursos estándar donde el primer argumento es la URL y el segundo es el nombre de la clase del controlador. Entonces ejecutaremos mix ecto.migrate

Salida:

 Compilando 11 archivos (.ex) 
Aplicación pxblog generada
 16: 02: 03.987 [info] == Ejecutando Pxblog.Repo.Migrations.CreateUser.change / 0 forward 
 16: 02: 03.987 [info] crear usuarios de tablas 
 16: 02: 03.996 [info] == Migrado en 0.0s 

Y finalmente, reinicie el servidor y revise http: // localhost: 4000 / users. ¡Ahora podemos crear publicaciones y usuarios de manera independiente! Desafortunadamente, este no es un blog muy útil todavía. Después de todo, podemos crear usuarios (cualquiera puede, de hecho), pero ni siquiera podemos iniciar sesión. Además, los resúmenes de contraseñas no provienen de ningún algoritmo de encriptación; ¡el usuario simplemente los está creando y los estamos almacenando en texto sin formato! ¡No bueno!

En su lugar, haremos que se vea más como una pantalla de registro de usuario real.

Nuestras pruebas para las cosas del usuario se ven exactamente iguales a las que se generaron automáticamente para nuestras publicaciones, así que las dejaremos en paz hasta que comencemos a modificar la lógica (¡ahora mismo!)

Paso 3: guardar un hash de contraseña en lugar de una contraseña

Cuando visitamos / users / new , vemos tres campos: Nombre de usuario , Correo electrónico y PasswordDigest . ¡Pero cuando se registra en otros sitios, debe ingresar una contraseña y una confirmación de contraseña! ¿Cómo podemos corregir esto?

En web / templates / user / form.html.eex , elimine las siguientes líneas:

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

Y agrega en su lugar:

 <div class = "form-group"> 
<% = etiqueta f,: contraseña, "Contraseña", clase: "control-label"%>
<% = contraseña_input f,: contraseña, clase: "form-control"%>
<% = error_tag f,: contraseña%>
</ div>

<div class = "form-group">
<% = label f,: password_confirmation, "Password Confirmation", clase: "control-label"%>
<% = password_input f,: password_confirmation, class: "form-control"%>
<% = error_tag f,: password_confirmation%>
</ div>

Actualice la página (debe suceder automáticamente), ingrese los detalles del usuario, presione enviar.

Error:

 ¡Huy! Algo salió mal! Por favor revise los errores a continuación. 

Esto se debe a que estamos creando una contraseña y una contraseña de confirmación, pero no se está haciendo nada para crear el password_digest real. Vamos a escribir un código para hacer esto. Primero, vamos a modificar el esquema real para hacer algo nuevo:

En web / models / user.ex :

 los "usuarios" de esquema 
campo: nombre de usuario,: ??cadena
campo: correo electrónico,: cadena
campo: password_digest,: string
 marcas de tiempo 
 # Campos virtuales 
campo: contraseña,: cadena, virtual: verdadero
campo: password_confirmation,: string, virtual: true
fin

Tenga en cuenta la adición de los dos campos: contraseña y confirmación de contraseña. Estamos declarando estos como campos virtuales , ya que en realidad no existen en nuestra base de datos, pero deben existir como propiedades en nuestra estructura de usuario. Esto también nos permite aplicar transformaciones en nuestra función de conjunto de cambios.

Luego modificamos la lista de campos obligatorios y casted para incluir: password y: password_confirmation .

 def changeset (struct, params \% {}) do 
estructura
|> cast (params, [: nombre de usuario,: ??correo electrónico,: contraseña,: contraseña_confirmación])
|> validate_required ([: nombre de usuario,: ??correo electrónico,: contraseña,: contraseña_confirmación])
fin

Si ejecuta test / models / user_test.exs en este punto, notará que nuestra prueba "changeset with valid attributes" está fallando. Esto se debe a que hemos solicitado la contraseña y la confirmación de contraseña, pero no hemos actualizado nuestro mapa de @valid_attrs para incluirlos. Cambiemos esa línea a:

 @valid_attrs% {email: " test@test.com ", contraseña: "test1234", contraseña_confirmación: "test1234", nombre de usuario: "testuser"} 

¡Nuestras pruebas modelo deberían estar listas para pasar! También necesitamos pasar nuestras pruebas de controlador. En test / controllers / user_controller_test.exs , haremos algunas modificaciones. Primero, distinguiremos entre los atributos de creación válidos y los atributos de búsqueda válidos:

 @valid_create_attrs% {email: " test@test.com ", contraseña: "test1234", contraseña_confirmación: "test1234", nombre de usuario: "testuser"} 
@valid_attrs% {email: " test@test.com ", nombre de usuario: "testuser"}

Luego modificaremos nuestra prueba de creación:

 prueba "crea recurso y redirige cuando los datos son válidos",% {conn: conn} do 
conn = post conn, user_path (conn,: create), usuario: @valid_create_attrs
assert redirected_to (conn) == user_path (conn,: index)
assert Repo.get_by (Usuario, @valid_attrs)
fin

Y nuestra prueba de actualización:

 prueba "actualiza el recurso elegido y redirige cuando los datos son válidos",% {conn: conn} do 
usuario = Repo.insert! %Usuario{}
conn = poner conn, user_path (conn,: actualizar, usuario), usuario: @valid_create_attrs
assert redirected_to (conn) == user_path (conn,: show, usuario)
assert Repo.get_by (Usuario, @valid_attrs)
fin

Con nuestras pruebas de nuevo en verde, tenemos que modificar la función del conjunto de cambios para cambiar nuestra contraseña en un resumen de contraseña sobre la marcha:

 def changeset (struct, params \% {}) do 
estructura
|> cast (params, [: nombre de usuario,: ??correo electrónico,: contraseña,: contraseña_confirmación])
|> validate_required ([: nombre de usuario,: ??correo electrónico,: contraseña,: contraseña_confirmación])
|> hash_password
fin
 defp hash_password (changeset) do 
conjunto de cambios
|> put_change (: password_digest, "ABCDE")
fin

En este momento simplemente estamos apagando el comportamiento de nuestra función de hash. El primer paso es asegurarnos de que podamos modificar nuestro conjunto de cambios a medida que avanzamos. Verifiquemos este comportamiento primero. Regrese a http: // localhost: 4000 / users en nuestro navegador, haga clic en "Nuevo usuario" y cree un nuevo usuario con todos los detalles. Cuando volvamos a la página de índice, deberíamos esperar ver al usuario creado con un valor de password_digest de "ABCDE".

Y ejecuta nuestras pruebas nuevamente para este archivo. Nuestras pruebas están aprobadas, pero no hemos agregado ninguna prueba para este nuevo trabajo hash_password . Agreguemos una prueba en nuestro conjunto de pruebas de modelo que agregará una prueba en el resumen de contraseña:

 prueba "el valor de password_digest se establece en un hash" do 
changeset = User.changeset (% User {}, @valid_attrs)
assert get_change (changeset,: password_digest) == "ABCDE"
fin

Este es un gran paso adelante, ¡pero no terriblemente bueno para la seguridad! Modifiquemos nuestros hashes para que sean hashes de contraseñas reales con Bcrypt, cortesía de la biblioteca de comeonin .

Primero, abra mix.exs y agregue : comeonin a nuestra lista de aplicaciones:

 aplicación de def hacer 
[mod: {Pxblog, []},
aplicaciones: [: phoenix,: phoenix_pubsub,: phoenix_html,: cowboy,: logger,: gettext,
: phoenix_ecto,: postgrex,: comeonin]]
fin

Y también necesitaremos modificar nuestra definición de deps:

 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", solo:: dev},
{: gettext, "~> 0.11"},
{: cowboy, "~> 1.0"},
{: comeonin, "~> 2.3"}]
fin

Lo mismo aquí, tenga en cuenta la adición de {: comeonin, "~> 2.3"} . Ahora, apaguemos el servidor que hemos estado ejecutando y ejecutemos mix deps.get . Si todo va bien (¡debería!), Entonces ahora debería poder volver a ejecutar iex -S mix phoenix.server para reiniciar su servidor.

Nuestro antiguo método hash_password es claro , pero lo necesitamos para cifrar nuestra contraseña. Dado que hemos agregado la biblioteca de comeonin , que nos proporciona un buen módulo Bcrypt con un método hashpwsalt, así que vamos a importar eso a nuestro modelo de usuario.

En web / models / user.ex , agregue la siguiente línea en la parte superior justo debajo de nuestro uso Pxblog.Web,: línea modelo :

 importar Comeonin.Bcrypt, solo: [hashpwsalt: 1] 

Lo que estamos haciendo aquí es introducir el módulo Bcrypt en el espacio de nombres de Comeonin e importar el método hashpwsalt con una razón de 1. Y ahora vamos a modificar nuestro método hash_password para que funcione.

 defp hash_password (changeset) do 
if password = get_change (changeset,: password) do
conjunto de cambios
|> put_change (: password_digest, hashpwsalt (contraseña))
más
conjunto de cambios
fin
fin

¡Intentemos crear un usuario nuevamente! Esta vez, después de ingresar nuestros datos de usuario, correo electrónico, contraseña y confirmación de contraseña, ¡deberíamos ver un compendio encriptado en el campo password_digest !

Ahora, vamos a querer trabajar en la función hash_password que agregamos. Lo primero que vamos a querer hacer es cambiar la configuración de nuestro entorno de prueba para asegurarnos de que nuestras pruebas no disminuyan drásticamente al trabajar con nuestro cifrado de contraseñas. Abra config / test.exs y agregue lo siguiente al final:

 config: comeonin, bcrypt_log_rounds: 4 

Esto configurará ComeOnIn cuando se encuentre en nuestro entorno de prueba para que no intente encriptar nuestra contraseña. ¡Como esto es solo para pruebas, no necesitamos nada súper seguro y preferiríamos cordura y velocidad! En config / prod.exs , querremos cambiar eso a:

 config: comeonin, bcrypt_log_rounds: 14 

Vamos a escribir una prueba para la llamada de comeonin. Lo haremos un poco menos específico; solo queremos verificar el cifrado. En test / models / user_test.exs :

 prueba "el valor de password_digest se establece en un hash" do 
changeset = User.changeset (% User {}, @valid_attrs)
afirme Comeonin.Bcrypt.checkpw (@ valid_attrspassword, Ecto.Changeset.get_change (changeset,: password_digest))
fin

Para obtener más cobertura de prueba, agreguemos un caso para manejar si la contraseña = get_change () no se golpea la línea:

 prueba "el valor de password_digest no se establece si la contraseña es nula" do 
changeset = User.changeset (% User {},% {email: "test@test.com", contraseña: nil, password_confirmation: nil, username: "test"})
refutar Ecto.Changeset.get_change (changeset,: password_digest)
fin

Como afirmar / refutar el uso de la veracidad, podemos ver si este bloque de código deja en blanco a password_digest , ¡que sí lo hace! ¡Estamos haciendo un buen trabajo cubriendo nuestro trabajo con especificaciones!

Paso 4: ¡Inicie sesión!

Agreguemos un nuevo controlador, SessionController y una vista de acompañamiento, SessionView. Comenzaremos de forma simple y construiremos nuestro camino hacia una mejor implementación a lo largo del tiempo.

Crear web / controladores / session_controller.ex:

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

Crear web / views / session_view.ex :

 defmodule Pxblog.SessionView do 
use Pxblog.Web,: vista
fin

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

 <h2> Iniciar sesión </ h2> 

Y finalmente, actualicemos el enrutador para incluir este nuevo controlador. Agregue la siguiente línea a nuestro alcance "/":

 recursos "/ sessions", SessionController, solo: [: new] 

La única ruta que queremos exponer por el momento es nueva , así que vamos a limitarla solo a eso. Una vez más, queremos mantener las cosas simples y construir desde una base estable.

Ahora visitemos http: // localhost: 4000 / sessions / new y deberíamos esperar ver el encabezado de Phoenix framework y el encabezado "Login".

Vamos a darle una forma real. Crear web / templates / session / form.html.eex :

 <% = form_for @changeset, @action, fn f ->%> 
<% = si f.errors! = [] do%>
<div class = "alert alert-danger">
<p> ¡Vaya, algo salió mal! Por favor revise los siguientes errores: </ p>
<ul>
<% = para {attr, mensaje} <- f.errors do%>
<li> <% = humanize (attr)%> <% = message%> </ li>
<% end%>
</ ul>
</ div>
<% end%>
 <div class = "form-group"> 
<label> Nombre de usuario </ label>
<% = text_input f,: nombre de usuario, clase: "form-control"%>
</ div>
 <div class = "form-group"> 
<etiqueta> Contraseña </ etiqueta>
<% = contraseña_input f,: contraseña, clase: "form-control"%>
</ div>
 <div class = "form-group"> 
<% = enviar "Enviar", clase: "btn btn-primary"%>
</ div>
<% end%>

Y modifique web / templates / session / new.html.eex para llamar a nuestro nuevo formulario agregando una línea:

 <% = render "form.html", conjunto de cambios: @changeset, action: session_path (@conn,: create)%> 

La recarga automática terminará mostrando una página de error en este momento porque no hemos definido realmente @changeset , que como puede adivinar debe ser un conjunto de cambios. Como estamos trabajando con el objeto miembro, que ya tiene campos de nombre de usuario y contraseña, ¡usemos eso!

En web / controllers / session_controller.ex , necesitamos alias del modelo de Usuario para poder usarlo más. En la parte superior de nuestra clase, bajo nuestro uso Pxblog.Web,: línea de controlador , agregue lo siguiente:

 alias Pxblog.Usuario 

Y en la nueva función, modifique la llamada para procesar de la siguiente manera:

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

Necesitamos pasarle la conexión, la plantilla que estamos procesando (menos el eex) y una lista de variables adicionales que deberían estar expuestas a nuestras plantillas. En este caso, queremos exponer @changeset, por lo que especificamos el conjunto de cambios: aquí, y le damos el conjunto de cambios de Ecto para el usuario con una estructura de usuario en blanco. (% User {} es una estructura de usuario sin valores establecidos)

Actualice ahora y obtenemos un mensaje de error diferente que debería parecerse a lo siguiente:

 No se ha definido una cláusula auxiliar para Pxblog.Router.Helpers.session_path / 2 para la acción: crear. 
Las siguientes acciones de session_path se definen debajo de su enrutador:
*:nuevo

En nuestra forma, estamos haciendo referencia a una ruta que en realidad aún no existe. Estamos utilizando el helper session_path, pasándole el objeto @conn, pero luego especificando: create path que aún no hemos creado.

Hemos conseguido parte del camino allí. Ahora, hagámoslo para que podamos publicar nuestros datos de acceso y configurar la sesión.

Actualicemos nuestras rutas para permitir la publicación para crear.

En web / router.ex , cambie nuestra referencia a SessionController para incluir también: create.

 recursos "/ sessions", SessionController, solo: [: new,: create] 

En web / controllers / session_controller.ex , necesitamos importar una nueva función, checkpw del módulo Bcrypt de Comeonin. Hacemos esto a través de la siguiente línea:

 importar Comeonin.Bcrypt, solo: [checkpw: 2] 

Esta línea dice "Importar desde el módulo Comeonin.Bcrypt, pero solo la función checkpw con una arity de 2". Y luego agreguemos un plugin scrub_params para tratar con los datos del usuario. Antes de nuestras funciones, agregaremos:

 plug: scrub_params, "usuario" cuando se realiza una acción en [: create] 

"Scrub_params" es una función especial que limpia cualquier entrada del usuario; en el caso de que algo se pase como una cadena en blanco, por ejemplo, scrub_params lo convertirá en un valor nulo para evitar la creación de entradas en su base de datos que tengan cadenas vacías.

Y agreguemos nuestra función para manejar la publicación de creación. Vamos a agregar esto al final de nuestro módulo SessionController. Aquí va a haber un montón de código, así que lo tomaremos pieza por pieza.

En web / controllers / session_controller.ex :

 def create (conn,% {"user" => user_params}) do 
Repo.get_by (Usuario, nombre de usuario: user_params ["username"])
|> sign_in (user_params ["password"], conn)
fin

El primer bit de este código, Repo.get_by (User, username: user_params ["username"]) saca el primer Usuario aplicable de nuestro Ecto Repo que tiene un nombre de usuario coincidente, o de lo contrario devuelve nil.

Aquí hay algunos resultados para verificar este comportamiento:

 iex (3)> Repo.get_by (Usuario, nombre de usuario: "flibbity") 
[debug] SELECT u0. "id", u0. "nombre de usuario", u0. "email", u0. "password_digest", u0. "inserted_at", u0. "updated_at" FROM "users" AS u0 WHERE (u0. " nombre de usuario "= $ 1) [" flibbity "] OK query = 0.7ms
nulo
 iex (4)> Repo.get_by (Usuario, nombre de usuario: "prueba") 
[debug] SELECT u0. "id", u0. "nombre de usuario", u0. "email", u0. "password_digest", u0. "inserted_at", u0. "updated_at" FROM "users" AS u0 WHERE (u0. " nombre de usuario "= $ 1) [" test "] OK query = 0.8ms
% Pxblog.User {__ meta__:% Ecto.Schema.Metadata {fuente: "usuarios", estado:: cargado},
correo electrónico: "prueba", id: 15,
inserted_at:% Ecto.DateTime {día: 24, hora: 19, min: 6, mes: 6, sec: 14,
usec: 0, año: 2015}, contraseña: 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, año: 2015}, nombre de usuario: "prueba"}

Luego tomamos al usuario y encadenamos a ese usuario en un método sign_in. No hemos escrito eso todavía, así que hagámoslo!

 defp sign_in (usuario, contraseña, conn) cuando is_nil (usuario) do 
conn
|> put_flash (: error, "combinación de nombre de usuario / contraseña inválida!")
|> redirigir (a: page_path (conn,: index))
fin
 defp sign_in (usuario, contraseña, conn) do 
if checkpw (password, user.password_digest) do
conn
|> put_session (: current_user,% {id: user.id, username: user.username})
|> put_flash (: información, "¡Iniciar sesión exitosamente!")
|> redirigir (a: page_path (conn,: index))
más
conn
|> put_session (: current_user, nil)
|> put_flash (: error, "combinación de nombre de usuario / contraseña inválida!")
|> redirigir (a: page_path (conn,: index))
fin
fin

Lo primero que debe observar es el orden en que se definen estos métodos. El primero de estos métodos tiene una cláusula de guardia adjunta, por lo que el método solo se ejecutará cuando esa cláusula de guardia sea verdadera, por lo que si el usuario es nulo, redirigir de nuevo al índice de la ruta de la página (raíz) con un mensaje flash apropiado.

Se llamará al segundo método si la cláusula de guardia es falsa y manejará todos los demás escenarios. Verificamos el resultado de esa función checkpw, y si es cierto, configuramos al usuario como la variable de sesión current_user y lo redirigimos con un mensaje de éxito. De lo contrario, borramos la sesión de usuario actual, establecemos un mensaje de error y redirigimos a la raíz.

Si volvemos a nuestra página de inicio de sesión http: // localhost: 4000 / sessions / new, ¡deberíamos poder probar el inicio de sesión con un conjunto válido de credenciales y credenciales no válidas y ver los mensajes de error apropiados!

Necesitamos escribir algunas especificaciones para este controlador, también. Crearemos test / controllers / session_controller_test.exs y lo llenaremos con lo siguiente:

 defmodule Pxblog.SessionControllerTest do 
usa Pxblog.ConnCase
alias Pxblog.Usuario
 configurar 
User.changeset (% User {},% {username: "test", password: "test", password_confirmation: "test", email: " test@test.com "})
|> Repo.insert
{: ok, conn: build_conn ()}
fin
 prueba "muestra el formulario de inicio de sesión",% {conn: conn} do 
conn = get conn, session_path (conn,: new)
assert html_response (conn, 200) = ~ "Iniciar sesión"
fin
 prueba "crea una nueva sesión de usuario para un usuario válido",% {conn: conn} do 
conn = post conn, session_path (conn,: create), usuario:% {username: "test", password: "test"}
assert get_session (conn,: current_user)
assert get_flash (conn,: info) == "¡Inicia sesión exitosamente!"
assert redirected_to (conn) == page_path (conn,: index)
fin
 prueba "no crea una sesión con un inicio de sesión incorrecto",% {conn: conn} do 
conn = post conn, session_path (conn,: create), usuario:% {username: "test", password: "wrong"}
refute get_session (conn,: current_user)
assert get_flash (conn,: error) == "¡Combinación de nombre de usuario / contraseña inválida!"
assert redirected_to (conn) == page_path (conn,: index)
fin
 test "no crea una sesión si el usuario no existe",% {conn: conn} do 
conn = post conn, session_path (conn,: create), usuario:% {username: "foo", contraseña: "wrong"}
assert get_flash (conn,: error) == "¡Combinación de nombre de usuario / contraseña inválida!"
assert redirected_to (conn) == page_path (conn,: index)
fin
fin

Comenzamos con nuestro bloque de configuración estándar y escribimos una aserción bastante estándar para una solicitud de obtención. Los bits de creación son donde esto comienza a ser más interesante:

 prueba "crea una nueva sesión de usuario para un usuario válido",% {conn: conn} do 
conn = post conn, session_path (conn,: create), usuario:% {username: "test", password: "test"}
assert get_session (conn,: current_user)
assert get_flash (conn,: info) == "¡Inicia sesión exitosamente!"
assert redirected_to (conn) == page_path (conn,: index)
fin

El primer bit aquí es publicar en nuestra ruta de creación de sesión. Luego verificamos que establezcamos la variable de sesión current_user, el mensaje flash para la acción y, finalmente, afirmamos a dónde nos redirigen.

Las otras dos llamadas solo usan el mismo tipo de aserciones (y en un caso, una refutación) para asegurarse de que estamos probando todas las rutas a las que puede acceder la función sign_in. De nuevo, ¡cosas muy simples!

Paso 5: Exponiendo nuestro usuario actual

Modifiquemos nuestro diseño para mostrar un mensaje o un enlace dependiendo de si el miembro está conectado o no.

En web / views / layout_view.ex , escribamos un helper que nos facilite el acceso a la información del usuario, por lo que agregaremos:

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

Vamos a escribir una prueba para asegurarnos de que esto funcione.

En web / templates / layout / app.html.eex , en lugar del enlace "Comenzar", hagamos lo siguiente:

 <li> 
<% = si usuario = usuario_actual (@conn) do%>
Conectado como
<strong> <% = user.username%> </ strong>
<br>
<% = link "Cerrar sesión", a: session_path (@conn,: delete, user.id), método:: delete%>
<% else%>
<% = link "Log in", a: session_path (@conn,: new)%>
<% end%>
</ li>

De nuevo, sigamos paso por paso. Una de las primeras cosas que debemos hacer es descubrir quién es el usuario actual, suponiendo que haya iniciado sesión. Vamos a utilizar un enfoque simple primero, de refactorización posterior, por lo que ahora mismo vamos a configurar un objeto de usuario de la sesión directamente en nuestra plantilla. get_session es parte del objeto Plug.Conn. Si el usuario existe (esto toma ventaja de los valores de verdad de Ruby de Elixir en ese nulo, aquí se devolverá falso).

Si el usuario ha iniciado sesión, también deseamos proporcionar un enlace de cierre de sesión . Aunque esto todavía no existe, eventualmente tendrá que existir, por lo que ahora mismo vamos a enviarlo. Trataremos una sesión como un recurso, por lo que para cerrar la sesión, "eliminaremos" la sesión, por lo que le proporcionaremos un enlace aquí.

También queremos dar salida al nombre de usuario del usuario actual. Estamos almacenando la estructura del usuario en la variable de sesión: current_user, de modo que solo podemos acceder al nombre de usuario como user.username.

Si no pudiéramos encontrar al usuario, entonces solo proporcionaremos el enlace de inicio de sesión. De nuevo, estamos tratando las sesiones como un recurso aquí, por lo que "nuevo" proporcionará la ruta adecuada para crear una nueva sesión.

Probablemente habrás notado que cuando todo se actualizó, recibiremos otro mensaje de error sobre la ausencia de una cláusula de función coincidente. ¡Agreguemos nuestra ruta de eliminación también para mantener a Phoenix feliz!

En web / router.ex , modificaremos nuestra ruta de "sesiones" para permitir también: eliminar:

 recursos "/ sessions", SessionController, solo: [: new,: create,: delete] 

Y modifiquemos el controlador también. En web / controllers / session_controller.ex , agregue lo siguiente:

 def delete (conn, _params) do 
conn
|> delete_session (: usuario_actual)
|> put_flash (: información, "¡Firmado con éxito!")
|> redirigir (a: page_path (conn,: index))
fin

Como solo estamos eliminando la clave: current_user, en realidad no nos importa qué son los params, así que los marcamos como no utilizados con un guión bajo. Configuramos un mensaje flash para que la interfaz de usuario sea un poco más clara para el usuario y redirigir a nuestra ruta raíz.

¡Ahora podemos iniciar sesión, cerrar sesión y ver errores de inicio de sesión también! ¡Las cosas se perfilan para mejor! Pero primero, tenemos que escribir algunas pruebas. Comenzaremos con las pruebas para nuestro LayoutView.

 defmodule Pxblog.LayoutViewTest do 
use Pxblog.ConnCase, async: true
 alias Pxblog.LayoutView 
alias Pxblog.Usuario
 configurar 
User.changeset (% User {},% {username: "test", password: "test", password_confirmation: "test", email: " test@test.com "})
|> Repo.insert
{: ok, conn: build_conn ()}
fin
 test "el usuario actual devuelve al usuario en la sesión",% {conn: conn} do 
conn = post conn, session_path (conn,: create), usuario:% {username: "test", password: "test"}
assert LayoutView.current_user (conn)
fin
 test "el usuario actual no devuelve nada si no hay ningún usuario en la sesión",% {conn: conn} do 
user = Repo.get_by (Usuario,% {username: "test"})
conn = eliminar conn, session_path (conn,: delete, usuario)
refutar LayoutView.current_user (conn)
fin
fin

Analicemos. Lo primero que vamos a hacer es alias los módulos LayoutView y User para poder acortar algo de nuestro código. A continuación, en nuestro bloque de configuración, creamos un usuario y lo lanzamos a la base de datos. Luego devolvemos nuestro estándar {: ok, conn: build_conn ()} tupla.

Luego, escribimos nuestra primera prueba iniciando sesión en nuestra acción de creación de sesión y afirmando que, después de iniciar sesión, la función LayoutView.current_user devuelve algunos datos.

Luego escribimos para nuestro caso negativo; eliminamos explícitamente la sesión y refutamos que un usuario se devuelve de nuestra llamada actual_user. También actualizamos nuestro SessionController al agregar una acción de eliminación, por lo que debemos tener todas las pruebas establecidas.

 prueba "borra la sesión del usuario",% {conn: conn} do 
user = Repo.get_by (Usuario,% {username: "test"})
conn = eliminar conn, session_path (conn,: delete, usuario)
refute get_session (conn,: current_user)
assert get_flash (conn,: info) == "¡Firmado con éxito!"
assert redirected_to (conn) == page_path (conn,: index)
fin

Nos aseguramos de que current_user de la sesión esté en blanco, luego verificamos el mensaje flash y ¡nos aseguramos de que seamos redirigidos!

Posibles errores con la compilación de activos

Una cosa a tener en cuenta es que puede terminar acertando un error cuando intenta compilar los activos con brunch. El mensaje de error que recibí fue:

 16 dic 23:30:20 - error: Falló la compilación de 'web / static / js / app.js'. No se pudo encontrar el preestablecido "es2015" relativo al directorio "web / static / js"; La compilación de 'web / static / js / socket.js' falló. No se pudo encontrar el preestablecido "es2015" relativo al directorio "web / static / js"; La compilación de 'deps / phoenix / web / static / js / phoenix.js' falló. No se pudo encontrar el preestablecido "es2015" relativo al directorio "deps / phoenix / web / static / js"; La compilación de 'deps / phoenix_html / web / static / js / phoenix_html.js' falló. No se pudo encontrar el preajuste "es2015" relativo al directorio "deps / phoenix_html / web / static / js" 

Puede solucionar esto instalando babel-pre-establecido-es2015 con NPM. Ejecuté el siguiente comando:

 npm install -g babel-preset-es2015 

¡Ahora, si inicias el servidor, deberías ver todos los activos correctamente compilados!

Siguiente publicación en esta serie

Escribir un motor de blog en Phoenix y Elixir: Parte 2, Autorización
Última actualización en: 21/07/2016 medium.com

¡Mira mi nuevo libro!

¡Hola a todos! Si le gustó lo que leyó aquí y quiere aprender más conmigo, consulte mi nuevo libro sobre el desarrollo web de Elixir y Phoenix:

Desarrollo web de Phoenix | Libros PACKT
Aprenda a construir un prototipo funcional de alto rendimiento de una aplicación web de votación desde cero utilizando Elixir y … www.packtpub.com

¡Estoy realmente emocionado de finalmente traer este proyecto al mundo! Está escrito en el mismo estilo que mis otros tutoriales, donde construiremos el andamio de un proyecto completo de principio a fin, incluso cubriendo algunos de los temas más complicados como la carga de archivos, los inicios de sesión de Twitter / Google OA y las API.

Hacker Noon es cómo los hackers comienzan sus tardes. Somos parte de la familia @AMI . Ahora estamos aceptando presentaciones y estamos felices de conversar sobre oportunidades de publicidad y patrocinio .

Para obtener más información, lea nuestra página acerca de , como / envíenos un mensaje en Facebook , o simplemente, tweet / DM @HackerNoon.

Si disfrutaste esta historia, te recomendamos que leas nuestras últimas historias tecnológicas e historias tecnológicas de tendencia . Hasta la próxima, ¡no des por sentado las realidades del mundo!