Erstellen einer Blog-Engine mit Phoenix und Elixir / Teil 2. Autorisierung

Ursprünglicher Autor: Brandon Richey
  • Übersetzung
  • Tutorial


Von einem Übersetzer: „ Elixir und Phoenix sind ein großartiges Beispiel dafür, wo moderne Webentwicklungen hingehen. Diese Tools bieten bereits einen hochwertigen Zugriff auf Echtzeit-Technologie für Webanwendungen. Websites mit erhöhter Interaktivität, Mehrbenutzer-Browsergames und Microservices sind die Bereiche, in denen diese Technologien gute Dienste leisten werden. Das Folgende ist eine Übersetzung einer Reihe von 11 Artikeln, die ausführlich Aspekte der Entwicklung auf dem Phoenix-Framework beschreiben. Es scheint eine so triviale Sache wie eine Blog-Engine zu sein. Aber beeilen Sie sich nicht, um zu stolpern, es wird wirklich interessant sein, besonders wenn die Artikel Sie auffordern, auf Elixir zu achten oder seine Anhänger zu werden.

In diesem Teil werden wir die Basis für das Blog fertigstellen, tiefer in das Testen eintauchen und schließlich die Autorisierung hinzufügen. Ich entschuldige mich für die kleine Verspätung, dann werde ich versuchen, mich an einen klaren Zeitplan zu halten oder den Zeitplan zu übertreffen!
»

Unsere Bewerbung basiert derzeit auf:

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

Wir beheben einige Fehler


Wenn Sie den ersten Teil befolgt haben, sollten Sie eine etwas funktionierende Blog-Engine auf Elixir / Phoenix haben. Wenn Sie wie ich sind, erregt und beschleunigt sogar ein so scheinbar kleiner Teil der geleisteten Arbeit Ihren Fortschritt, dass Sie den Code weiter verbessern möchten.

Wenn Sie den Fortschritt der Arbeit verfolgen möchten, habe ich den gesamten Code für Sie in das Repository von Github hochgeladen .

Der erste Fehler kann leicht reproduziert werden, indem Sie die Adresse http: // localhost: 4000 / sessions / new aufrufen und auf die Schaltfläche Submit klicken . Es sollte eine Fehlermeldung ähnlich der folgenden angezeigt werden:

nil given for :username, comparison with nil is forbidden as it always evaluates to false. Pass a full query expression and use is_nil/1 instead.

Wenn wir uns die Erstellungsfunktion in SessionController ansehen, wird sofort klar, was los ist.

def create(conn, %{"user" => user_params}) do
  user = Repo.get_by(User, username: user_params["username"])
  user
  |> sign_in(user_params["password"], conn)
end

Wenn wir also in den Parametern anstelle des Benutzernamens eine Zeichenfolge mit einem leeren Wert (oder nichts) senden , erhalten wir eine Fehlermeldung. Lassen Sie es uns schnell beheben. Glücklicherweise ist dies mit Hilfe der Schutzklausel und des Mustervergleichs leicht möglich . Ersetzen Sie die aktuelle Erstellungsfunktion durch Folgendes:

def create(conn, %{"user" => %{"username" => username, "password" => password}})
when not is_nil(username) and not is_nil(password) do
  user = Repo.get_by(User, username: username)
  sign_in(user, password, conn)
end
def create(conn, _) do
  failed_login(conn)
end

Wir ersetzen das Argument params in der zweiten Funktion create durch den Unterstrich, da wir es nirgendwo verwenden müssen. Wir verweisen auch auf die Funktion failed_login , die als privat hinzugefügt werden muss. In der Datei web / controller / session_controller.ex ändern wir den Comeonin- Import :

import Comeonin.Bcrypt, only: [checkpw: 2, dummy_checkpw: 0]

Wir müssen dummy_checkpw () aufrufen, damit niemand durch einfache Aufzählung von Benutzern einen Zeitangriff starten kann. Als nächstes fügen wir die failed_login Funktion hinzu :

defp failed_login(conn) do
  dummy_checkpw()
  conn
  |> put_session(:current_user, nil)
  |> put_flash(:error, "Invalid username/password combination!")
  |> redirect(to: page_path(conn, :index))
  |> halt()
end

Beachten Sie auch hier den Aufruf von dummy_checkpw () oben! Wir löschen auch unsere current_user- Sitzung , setzen eine Flash-Nachricht, die den Benutzer über das falsche Login und Passwort informiert, und leiten es zurück zur Hauptseite. Am Ende nennen wir die Stopp- Funktion , die eine sinnvolle Abwehr gegen Probleme mit dem Doppelrendering darstellt. Und dann ersetzen wir den gesamten ähnlichen Code durch Aufrufe unserer neuen Funktion.

defp sign_in(user, _password, conn) when is_nil(user) do
  failed_login(conn)
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
    failed_login(conn)
  end
end

Diese Änderungen sollten sich um alle vorhandenen seltsamen Anmeldefehler kümmern, damit wir den Benutzern, die sie hinzufügen, Beiträge zuordnen können.

Migration hinzufügen


Beginnen wir mit dem Hinzufügen eines Links zur Benutzertabelle in der Beitragstabelle . Erstellen Sie dazu über den ECTO-Generator eine Migration:

$ mix ecto.gen.migration add_user_id_to_posts

Fazit:

Compiling 1 file (.ex)
* creating priv/repo/migrations
* creating priv/repo/migrations/20160720211140_add_user_id_to_posts.exs

Wenn wir die gerade erstellte Datei öffnen, sehen wir nichts darin. Fügen Sie der Änderungsfunktion also den folgenden Code hinzu:

def change do
  alter table(:posts) do
    add :user_id, references(:users)
  end
  create index(:posts, [:user_id])
end

Dadurch wird die user_id- Spalte hinzugefügt , die sich auf die Benutzertabelle bezieht , sowie der Index dafür. Wir werden den Befehl ausführen mix ecto.migrateund mit der Bearbeitung unserer Modelle fortfahren.

Wir verbinden Beiträge mit Nutzern


Lassen Sie uns die Datei web / models / post.ex öffnen und einen Link zum Benutzermodell hinzufügen . Platzieren Sie innerhalb des Beitragsschemas die Zeile:

belongs_to :user, Pxblog.User

Wir müssen dem Benutzermodell ein Feedback hinzufügen, das auf das Post- Modell verweist . Innerhalb Schema - Benutzer in der Datei web / models / user.ex platzieren Sie die Zeile:

has_many :posts, Pxblog.Post

Wir müssen auch den Posts- Controller öffnen und Posts direkt mit Benutzern verknüpfen.

Wir ändern den Weg


Beginnen wir mit der Aktualisierung des Routers, indem wir Beiträge innerhalb der Benutzer angeben. Öffnen Sie dazu die Datei web / router.ex und ersetzen Sie die Pfade / users und / posts durch:

resources "/users", UserController do
  resources "/posts", PostController
end

Wir reparieren den Controller


Wenn wir versuchen, den Befehl mix phoenix.routesjetzt auszuführen , erhalten wir eine Fehlermeldung. Das ist die Norm! Da wir die Struktur der Pfade geändert haben, haben wir den Helfer post_path verloren , dessen neue Version user_post_path heißt und auf die angehängte Ressource verweist. Verschachtelte Helfer ermöglichen den Zugriff auf Pfade, die durch Ressourcen dargestellt werden, für die eine andere Ressource erforderlich ist (für Beiträge ist beispielsweise ein Benutzer erforderlich).

Wenn wir also einen regulären post_path- Helfer haben , nennen wir das so:

post_path(conn, :show, post)

Das conn- Objekt ist das Verbindungsobjekt, atom : show ist die Aktion, auf die wir uns beziehen, und das dritte Argument kann entweder eine Modell- oder eine Objektkennung sein. Ab hier haben wir die Möglichkeit dazu:

post_path(conn, :show, 1)

Wenn wir über eine verschachtelte Ressource verfügen, ändern sich gleichzeitig die Helfer und unsere Routendatei. In unserem Fall:

user_post_path(conn, :show, user, post)

Beachten Sie, dass das dritte Argument jetzt eine externe Ressource darstellt und jede verschachtelte Ressource als Nächstes kommt.

Nachdem wir verstanden haben, warum Fehler auftreten, können wir sie beheben. Wir müssen in jeder der Controller-Aktionen Zugriff auf den angeforderten Benutzer haben. Der beste Weg, um es zu bekommen, ist die Verwendung eines Plugins. Öffnen Sie dazu die Datei web / controller / post_controller.ex und fügen Sie dem neuen Plugin ganz oben einen Aufruf hinzu:

plug :assign_user

Und wir werden es etwas tiefer schreiben:

defp assign_user(conn, _opts) do
  case conn.params do
    %{"user_id" => user_id} ->
      user = Repo.get(Pxblog.User, user_id)
      assign(conn, :user, user)
    _ ->
      conn
  end
end

Und dann ersetzen wir überall post_path durch user_post_path :

def create(conn, %{"post" => post_params}) do
 changeset = Post.changeset(%Post{}, post_params)
  case Repo.insert(changeset) do
    {:ok, _post} ->
      conn
      |> put_flash(:info, "Post created successfully.")
      |> redirect(to: user_post_path(conn, :index, conn.assigns[:user]))
    {:error, changeset} ->
      render(conn, "new.html", changeset: changeset)
  end
end
def update(conn, %{"id" => id, "post" => post_params}) do
  post = Repo.get!(Post, id)
  changeset = Post.changeset(post, post_params)
  case Repo.update(changeset) do
    {:ok, post} ->
      conn
      |> put_flash(:info, "Post updated successfully.")
      |> redirect(to: user_post_path(conn, :show, conn.assigns[:user], post))
    {:error, changeset} ->
      render(conn, "edit.html", post: post, changeset: changeset)
  end
end
def delete(conn, %{"id" => id}) do
  post = Repo.get!(Post, id)
  # Здесь мы используем delete! (с восклицательным знаком), потому что мы ожидаем
  # что код всегда будет работать (иначе возникнет ошибка).
  Repo.delete!(post)
  conn
  |> put_flash(:info, "Post deleted successfully.")
  |> redirect(to: user_post_path(conn, :index, conn.assigns[:user]))
end

Bereinigen Sie die Vorlagen


Unser Controller hat die Fehlermeldung nicht mehr ausgegeben. Jetzt arbeiten wir an unseren Vorlagen. Wir sind einen kurzen Weg gegangen, indem wir ein Plug-In implementiert haben, auf das von jeder Controller-Aktion aus zugegriffen werden kann. Mit der Zuweisungsfunktion für das Verbindungsobjekt definieren wir eine Variable, mit der wir in der Vorlage arbeiten können. Lassen Sie uns nun die Vorlagen ein wenig ändern, indem wir den post_path-Helfer durch user_post_path ersetzen und sicherstellen, dass das nächste Argument nach dem Aktionsnamen die Benutzerkennung ist. Schreiben Sie in die Datei web / templates / post / index.html.eex :

Listing posts

<%= for post <- @posts do %> <% end %>
TitleBody
<%= post.title %><%= post.body %> <%= link "Show", to: user_post_path(@conn, :show, @user, post), class: "btn btn-default btn-xs" %> <%= link "Edit", to: user_post_path(@conn, :edit, @user, post), class: "btn btn-default btn-xs" %> <%= link "Delete", to: user_post_path(@conn, :delete, @user, post), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %>
<%= link "New post", to: user_post_path(@conn, :new, @user) %>

In der Datei web / templates / post / show.html.eex :

Show post

  • Title: <%= @post.title %>
  • Body: <%= @post.body %>
<%= link "Edit", to: user_post_path(@conn, :edit, @user, @post) %> <%= link "Back", to: user_post_path(@conn, :index, @user) %>

In der Datei web / templates / post / new.html.eex :

New post

<%= render "form.html", changeset: @changeset, action: user_post_path(@conn, :create, @user) %> <%= link "Back", to: user_post_path(@conn, :index, @user) %>

In der Datei web / templates / post / edit.html.eex :

Edit post

<%= render "form.html", changeset: @changeset, action: user_post_path(@conn, :update, @user, @post) %> <%= link "Back", to: user_post_path(@conn, :index, @user) %>

Wenn wir jetzt einen Health-Check durchführen, mix phoenix.routessollten wir die Ausgabe der Pfade und eine erfolgreiche Kompilierung sehen!

Compiling 14 files (.ex)
     page_path  GET     /                               Pxblog.PageController :index
     user_path  GET     /users                          Pxblog.UserController :index
     user_path  GET     /users/:id/edit                 Pxblog.UserController :edit
     user_path  GET     /users/new                      Pxblog.UserController :new
     user_path  GET     /users/:id                      Pxblog.UserController :show
     user_path  POST    /users                          Pxblog.UserController :create
     user_path  PATCH   /users/:id                      Pxblog.UserController :update
                PUT     /users/:id                      Pxblog.UserController :update
     user_path  DELETE  /users/:id                      Pxblog.UserController :delete
user_post_path  GET     /users/:user_id/posts           Pxblog.PostController :index
user_post_path  GET     /users/:user_id/posts/:id/edit  Pxblog.PostController :edit
user_post_path  GET     /users/:user_id/posts/new       Pxblog.PostController :new
user_post_path  GET     /users/:user_id/posts/:id       Pxblog.PostController :show
user_post_path  POST    /users/:user_id/posts           Pxblog.PostController :create
user_post_path  PATCH   /users/:user_id/posts/:id       Pxblog.PostController :update
                PUT     /users/:user_id/posts/:id       Pxblog.PostController :update
user_post_path  DELETE  /users/:user_id/posts/:id       Pxblog.PostController :delete
  session_path  GET     /sessions/new                   Pxblog.SessionController :new
  session_path  POST    /sessions                       Pxblog.SessionController :create
  session_path  DELETE  /sessions/:id                   Pxblog.SessionController :delete

Wir verbinden die restlichen Teile mit der Steuerung


Jetzt müssen wir nur noch die Arbeit an der Steuerung beenden, um die neuen Zuordnungen zu verwenden. Beginnen wir damit, die interaktive Konsole mit einem Team iex -S mixzu starten , um etwas über die Auswahl von Benutzerbeiträgen zu lernen. Zuvor müssen wir jedoch eine Liste der Standardimporte / -aliase erstellen, die jedes Mal geladen werden, wenn die iex-Konsole in unserem Projekt geladen wird. Erstellen Sie eine neue .iex.exs- Datei im Projektstamm (beachten Sie den Punkt am Anfang des Dateinamens) und füllen Sie sie mit folgendem Inhalt:

import Ecto.Query
alias Pxblog.User
alias Pxblog.Post
alias Pxblog.Repo
import Ecto

Jetzt müssen wir beim Starten von iex nicht jedes Mal so etwas tun:

iex(1)> import Ecto.Query
nil
iex(2)> alias Pxblog.User
nil
iex(3)> alias Pxblog.Post
nil
iex(4)> alias Pxblog.Repo
nil
iex(5)> import Ecto
nil

Jetzt müssen wir mindestens einen Benutzer im Repository haben. Ist dies nicht der Fall, fügen Sie es hinzu. Dann können wir laufen:

iex(8)> user = Repo.get(User, 1)
    [debug] SELECT u0."id", u0."username", u0."email", u0."password_digest", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [1] OK query=8.2ms
    %Pxblog.User{__meta__: #Ecto.Schema.Metadata<:loaded>, email: "test", id: 1,
     inserted_at: #Ecto.DateTime<2015-10-06T17:47:07Z>, password: nil,
     password_confirmation: nil,
     password_digest: "$2b$12$pV/XBBCRl0RQhadQd9Y4mevOy5y0j4bCC/LjGgx7VJMosRdwme22a",
     posts: #Ecto.Association.NotLoaded,
     updated_at: #Ecto.DateTime<2015-10-06T17:47:07Z>, username: "test"}
iex(10)> Repo.all(assoc(user, :posts))
    [debug] SELECT p0."id", p0."title", p0."body", p0."user_id", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 WHERE (p0."user_id" IN ($1)) [1] OK query=3.5ms
    []

Bisher haben wir noch keinen einzigen Beitrag für diesen Benutzer erstellt. Daher ist es logisch, hier eine leere Liste zu erstellen. Wir haben die Assoc- Funktion von Ecto verwendet , um eine Anfrage zu erhalten, die Beiträge mit dem Benutzer verknüpft. Wir können auch Folgendes tun:

iex(14)> Repo.all from p in Post,
...(14)>          join: u in assoc(p, :user),
...(14)>          select: p
[debug] SELECT p0."id", p0."title", p0."body", p0."user_id", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 INNER JOIN "users" AS u1 ON u1."id" = p0."user_id" [] OK query=0.9ms

Hier wird eine Anforderung mit innerer Verknüpfung anstelle einer direkten Bedingung für die Auswahl nach Benutzer-ID erstellt. Achten Sie besonders darauf, wie die in beiden Fällen generierten Abfragen aussehen. Es ist sehr nützlich, den SQL-Code zu verstehen, der "hinter den Kulissen" erstellt wird, wenn Sie mit dem Code arbeiten, der die Abfragen generiert.

Wir können die Preload- Funktion auch zum Abrufen von Beiträgen verwenden, um Benutzer vorab zu laden, wie unten gezeigt:

iex(18)> Repo.all(from u in User, preload: [:posts])
[debug] SELECT u0."id", u0."username", u0."email", u0."password_digest", u0."inserted_at", u0."updated_at" FROM "users" AS u0 [] OK query=0.9ms
[debug] SELECT p0."id", p0."title", p0."body", p0."user_id", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 WHERE (p0."user_id" IN ($1)) ORDER BY p0."user_id" [1] OK query=0.8ms
iex(20)> Repo.all(from p in Post, preload: [:user])
[debug] SELECT p0."id", p0."title", p0."body", p0."user_id", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 [] OK query=0.8ms
[]

Wir müssen Beiträge hinzufügen, damit wir mit Anfragen basteln können. Deshalb werden wir eine Ecto-Funktion namens build_assoc verwenden . Diese Funktion verwendet das erste Argument für das Modell, für das wir eine Assoziation hinzufügen möchten, und das zweite - die Assoziation selbst in Form eines Atoms.

iex(1)> user = Repo.get(User, 1)
iex(2)> post = build_assoc(user, :posts, %{title: "Test Title", body: "Test Body"})
iex(3)> Repo.insert(post)
iex(4)> posts = Repo.all(from p in Post, preload: [:user])

Und jetzt, nachdem wir die letzte Anfrage abgeschlossen haben, sollten wir die folgende Ausgabe bekommen:

iex(4)> posts = Repo.all(from p in Post, preload: [:user])
[debug] SELECT p0."id", p0."title", p0."body", p0."user_id", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 [] OK query=0.7ms
[debug] SELECT u0."id", u0."username", u0."email", u0."password_digest", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" IN ($1)) [1] OK query=0.7ms
[%Pxblog.Post{__meta__: #Ecto.Schema.Metadata<:loaded>, body: "Test Body",
  id: 1, inserted_at: #Ecto.DateTime<2015-10-06T18:06:20Z>, title: "Test Title",
  updated_at: #Ecto.DateTime<2015-10-06T18:06:20Z>,
  user: %Pxblog.User{__meta__: #Ecto.Schema.Metadata<:loaded>, email: "test",
   id: 1, inserted_at: #Ecto.DateTime<2015-10-06T17:47:07Z>, password: nil,
   password_confirmation: nil,
   password_digest: "$2b$12$pV/XBBCRl0RQhadQd9Y4mevOy5y0j4bCC/LjGgx7VJMosRdwme22a",
   posts: #Ecto.Association.NotLoaded,
   updated_at: #Ecto.DateTime<2015-10-06T17:47:07Z>, username: "test"},
  user_id: 1}]

Und wir überprüfen nur schnell das erste Ergebnis:

iex(5)> post = List.first posts
%Pxblog.Post{__meta__: #Ecto.Schema.Metadata<:loaded>, body: "Test Body", id: 1,
 inserted_at: #Ecto.DateTime<2015-10-06T18:06:20Z>, title: "Test Title",
 updated_at: #Ecto.DateTime<2015-10-06T18:06:20Z>,
 user: %Pxblog.User{__meta__: #Ecto.Schema.Metadata<:loaded>, email: "test",
  id: 1, inserted_at: #Ecto.DateTime<2015-10-06T17:47:07Z>, password: nil,
  password_confirmation: nil,
  password_digest: "$2b$12$pV/XBBCRl0RQhadQd9Y4mevOy5y0j4bCC/LjGgx7VJMosRdwme22a",
  posts: #Ecto.Association.NotLoaded,
  updated_at: #Ecto.DateTime<2015-10-06T17:47:07Z>, username: "test"},
 user_id: 1}
iex(6)> post.title
"Test Title"
iex(7)> post.user.username
"test"

Cool! Unser Experiment zeigte genau das, was wir erwartet haben , so kommen zurück an den Controller (Datei von Web / einem Controller / post_controller.ex ) und beginnen , den Code zu bearbeiten. In der Indexaktion möchten wir alle Beiträge erhalten, die sich auf den Benutzer beziehen. Beginnen wir damit:

def index(conn, _params) do
  posts = Repo.all(assoc(conn.assigns[:user], :posts))
  render(conn, "index.html", posts: posts)
end

Jetzt können wir die Liste der Beiträge für den ersten Benutzer sehen! Aber wenn wir versuchen , eine Liste der Beiträge für einen Benutzer zu bekommen , die nicht existiert, wir eine Fehlermeldung erhalten , die schlechte UX ist, so lassen uns , um unsere Stecker geben assign_user :

defp assign_user(conn, _opts) do
  case conn.params do
    %{"user_id" => user_id} ->
      case Repo.get(Pxblog.User, user_id) do
        nil  -> invalid_user(conn)
        user -> assign(conn, :user, user)
      end
    _ -> invalid_user(conn)
  end
end
defp invalid_user(conn) do
  conn
  |> put_flash(:error, "Invalid user!")
  |> redirect(to: page_path(conn, :index))
  |> halt
end

Wenn wir nun die Liste der Beiträge für einen nicht existierenden Benutzer öffnen, erhalten wir eine nette Flash-Nachricht und werden freundlich zu page_path weitergeleitet . Als nächstes müssen wir die neue Aktion ändern :

def new(conn, _params) do
  changeset =
    conn.assigns[:user]
    |> build_assoc(:posts)
    |> Post.changeset()
  render(conn, "new.html", changeset: changeset)
end

Wir nehmen das Benutzermodell , übergeben es an die build_assoc- Funktion und sagen, dass wir einen Beitrag erstellen müssen. Anschließend übergeben wir das resultierende leere Modell an die Post.changeset- Funktion , um eine leere Revision zu erhalten. Wir werden den gleichen Weg für die create-Methode gehen (mit Ausnahme des Hinzufügens von post_params ):

def create(conn, %{"post" => post_params}) do
  changeset =
    conn.assigns[:user]
    |> build_assoc(:posts)
    |> Post.changeset(post_params)
  case Repo.insert(changeset) do
    {:ok, _post} ->
      conn
      |> put_flash(:info, "Post created successfully.")
      |> redirect(to: user_post_path(conn, :index, conn.assigns[:user]))
    {:error, changeset} ->
      render(conn, "new.html", changeset: changeset)
  end
end

Und ändern Sie dann die Aktionen zum Anzeigen , Bearbeiten , Aktualisieren und Löschen :

def show(conn, %{"id" => id}) do
  post = Repo.get!(assoc(conn.assigns[:user], :posts), id)
  render(conn, "show.html", post: post)
end
def edit(conn, %{"id" => id}) do
  post = Repo.get!(assoc(conn.assigns[:user], :posts), id)
  changeset = Post.changeset(post)
  render(conn, "edit.html", post: post, changeset: changeset)
end
def update(conn, %{"id" => id, "post" => post_params}) do
  post = Repo.get!(assoc(conn.assigns[:user], :posts), id)
  changeset = Post.changeset(post, post_params)
  case Repo.update(changeset) do
    {:ok, post} ->
      conn
      |> put_flash(:info, "Post updated successfully.")
      |> redirect(to: user_post_path(conn, :show, conn.assigns[:user], post))
    {:error, changeset} ->
      render(conn, "edit.html", post: post, changeset: changeset)
  end
end
def delete(conn, %{"id" => id}) do
  post = Repo.get!(assoc(conn.assigns[:user], :posts), id)
  # Здесь мы используем delete! (с восклицательным знаком), потому что мы ожидаем
  # что оно всегда будет работать (иначе возникнет ошибка).
  Repo.delete!(post)
  conn
  |> put_flash(:info, "Post deleted successfully.")
  |> redirect(to: user_post_path(conn, :index, conn.assigns[:user]))
end

Nachdem wir alle Tests durchgeführt haben, sollten wir sehen, dass alles funktioniert. Außer dass ... jeder Benutzer die Möglichkeit hat, einen neuen Beitrag unter einem beliebigen Benutzer zu löschen, zu bearbeiten oder zu erstellen!

Wir beschränken die Erstellung von Beiträgen durch Benutzer


Wir können keine Blog-Engine mit einer solchen Sicherheitslücke veröffentlichen. Lassen Sie uns dies beheben, indem Sie ein weiteres Plugin hinzufügen, das sicherstellt, dass der empfangene Benutzer auch der aktuelle Benutzer ist.

Fügen Sie am Ende der Datei web / controller / post_controller.ex eine neue Funktion hinzu :

defp authorize_user(conn, _opts) do
    user = get_session(conn, :current_user)
    if user && Integer.to_string(user.id) == conn.params["user_id"] do
      conn
    else
      conn
      |> put_flash(:error, "You are not authorized to modify that post!")
      |> redirect(to: page_path(conn, :index))
      |> halt()
    end
  end

Fügen Sie ganz oben einen Plugin-Aufruf hinzu:

plug :authorize_user when action in [:new, :create, :update, :edit, :delete]

Jetzt sollte alles super funktionieren! Benutzer müssen registriert sein, um posten zu können, und dann nur mit ihnen arbeiten. Wir müssen lediglich die Testsuite aktualisieren, um diese Änderungen zu verarbeiten, und alles ist bereit. Um zu beginnen, führen Sie einfach einen Mischtest durch , um die aktuelle Situation zu bewerten. Wahrscheinlich wird dieser Fehler angezeigt:

** (CompileError) test/controllers/post_controller_test.exs:14: function post_path/2 undefined
    (stdlib) lists.erl:1337: :lists.foreach/2
    (stdlib) erl_eval.erl:669: :erl_eval.do_apply/6
    (elixir) lib/code.ex:363: Code.require_file/2
    (elixir) lib/kernel/parallel_require.ex:50: anonymous fn/4 in Kernel.ParallelRequire.spawn_requires/5

Leider müssen wir jeden post_path- Aufruf erneut in user_post_path ändern . Und dazu müssen wir unsere Tests radikal ändern. Wir beginnen damit, der Datei test / controller / post_controller_text.exs einen Einstellungsblock hinzuzufügen :

alias Pxblog.User
setup do
  {:ok, user} = create_user
  conn = build_conn()
  |> login_user(user)
  {:ok, conn: conn, user: user}
end
defp create_user do
  User.changeset(%User{}, %{email: "test@test.com", username: "test", password: "test", password_confirmation: "test"})
  |> Repo.insert
end
defp login_user(conn, user) do
  post conn, session_path(conn, :create), user: %{username: user.username, password: user.password}
end

Hier ist viel los. Als erstes haben wir der Funktion create_user einen Aufruf hinzugefügt , den wir schreiben müssen. Wir brauchen einige Helfer für die Tests, also fügen wir sie hinzu. Die Funktion create_user fügt Repo einfach einen Testbenutzer hinzu , weshalb wir beim Aufruf dieser Funktion den Mustervergleich {: ok, user} verwenden.

Als nächstes rufen wir wie zuvor conn = build_conn () auf . Übergeben Sie als Nächstes das Ergebnis von conn an die Funktion login_user . Dies verbindet Beiträge mit unserer Login-Funktion, da für alle grundlegenden Post-Aktionen ein Benutzer erforderlich ist. Es ist wichtig zu verstehen , dass wir zurückkommen müssen connund bei jedem Test dabei haben. Andernfalls bleibt der Benutzer nicht angemeldet.

Schließlich haben wir die Rückgabe dieser Funktion in die Rückgabe der Standardwerte geändert : ok und : conn , aber jetzt werden wir auch einen weiteren Eintrag : user in das Wörterbuch aufnehmen. Werfen wir einen Blick auf den ersten Test, den wir ändern werden:

test "lists all entries on index", %{conn: conn, user: user} do
  conn = get conn, user_post_path(conn, :index, user)
  assert html_response(conn, 200) =~ "Listing posts"
end

Bitte beachten Sie, dass wir das zweite Argument der Testmethode geändert haben, um mithilfe eines Mustervergleichs ein Wörterbuch zu erhalten, das neben key : conn auch key : user enthält . Dies stellt sicher, dass wir den : Benutzerschlüssel verwenden , mit dem wir im Setup- Block arbeiten . Außerdem haben wir den Aufruf des post_path- Helfers in user_post_path geändert und den Benutzer mit dem dritten Argument hinzugefügt. Führen Sie jetzt nur diesen Test direkt aus. Dies kann durch Angabe eines Tags oder durch Angabe der Nummer der gewünschten Zeile geschehen, indem der Befehl folgendermaßen ausgeführt wird:

$ mix test test/controller/post_controller_test.exs:[line number]

Unser Test sollte grün werden! Großartig! Nun wollen wir dieses Stück ändern:

test "renders form for new resources", %{conn: conn, user: user} do
  conn = get conn, user_post_path(conn, :new, user)
  assert html_response(conn, 200) =~ "New post"
end

Hier gibt es nichts Neues, außer den Setup- Handler und den Pfad zu ändern. Fahren Sie also fort.

test "creates resource and redirects when data is valid", %{conn: conn, user: user} do
  conn = post conn, user_post_path(conn, :create, user), post: @valid_attrs
  assert redirected_to(conn) == user_post_path(conn, :index, user)
  assert Repo.get_by(assoc(user, :posts), @valid_attrs)
end

Vergessen Sie nicht, dass wir jeden mit dem Benutzer verknüpften Beitrag erhalten mussten, damit wir alle Aufrufe von post_path ändern .

test "does not create resource and renders errors when data is invalid", %{conn: conn, user: user} do
  conn = post conn, user_post_path(conn, :create, user), post: @invalid_attrs
  assert html_response(conn, 200) =~ "New post"
end

Ein weiterer leicht veränderter Test. Es gibt nichts zu sehen, also gehen wir zum nächsten interessanteren über. Erinnern Sie sich noch einmal daran, dass wir Posts erstellen / empfangen, die zu Benutzerverbänden gehören, und dann den Test "Ausgewählte Ressource anzeigen" ändern :

test "shows chosen resource", %{conn: conn, user: user} do
  post = build_post(user)
  conn = get conn, user_post_path(conn, :show, user, post)
  assert html_response(conn, 200) =~ "Show post"
end

Zuvor haben wir Posts mit einem einfachen hinzugefügt Repo.insert! %Post{}. Dies wird nicht mehr funktionieren, daher müssen wir sie jetzt mit der richtigen Zuordnung erstellen. Da diese Zeile in den restlichen Tests häufig verwendet wird, werden wir einen Helfer schreiben, um die Verwendung zu vereinfachen.

defp build_post(user) do
  changeset =
    user
    |> build_assoc(:posts)
    |> Post.changeset(@valid_attrs)
  Repo.insert!(changeset)
end

Diese Methode erstellt ein gültiges Post-Modell, das dem Benutzer zugeordnet ist, und fügt es dann in die Datenbank ein. Bitte beachten Sie, dass Repo.insert! gibt nicht {: ok, model} zurück , sondern das Modell selbst!

Kehren wir zu unserem Test zurück, den wir geändert haben. Ich möchte den Rest der Tests auslegen, und Sie wiederholen einfach die entsprechenden Änderungen nacheinander, bis alle Tests bestanden sind.

test "renders page not found when id is nonexistent", %{conn: conn, user: user} do
    assert_raise Ecto.NoResultsError, fn ->
      get conn, user_post_path(conn, :show, user, -1)
    end
  end
 test "renders form for editing chosen resource", %{conn: conn, user: user} do
    post = build_post(user)
    conn = get conn, user_post_path(conn, :edit, user, post)
    assert html_response(conn, 200) =~ "Edit post"
  end
  test "updates chosen resource and redirects when data is valid", %{conn: conn, user: user} do
    post = build_post(user)
    conn = put conn, user_post_path(conn, :update, user, post), post: @valid_attrs
    assert redirected_to(conn) == user_post_path(conn, :show, user, post)
    assert Repo.get_by(Post, @valid_attrs)
  end
  test "does not update chosen resource and renders errors when data is invalid", %{conn: conn, user: user} do
    post = build_post(user)
    conn = put conn, user_post_path(conn, :update, user, post), post: %{"body" => nil}
    assert html_response(conn, 200) =~ "Edit post"
  end
  test "deletes chosen resource", %{conn: conn, user: user} do
    post = build_post(user)
    conn = delete conn, user_post_path(conn, :delete, user, post)
    assert redirected_to(conn) == user_post_path(conn, :index, user)
    refute Repo.get(Post, post.id)
  end

Wenn Sie alle Probleme behoben haben, können Sie den Befehl mix test ausführen und die grünen Tests abrufen!

Schließlich haben wir einige neue Codes geschrieben, z. B. Plugins für die Suche und Autorisierung von Benutzern, und wir haben erfolgreiche Fälle ziemlich gut getestet, aber wir müssen auch Tests für negative Fälle hinzufügen. Wir beginnen mit einem Test, was passiert, wenn wir versuchen, auf Beiträge eines Benutzers zuzugreifen, der nicht existiert.

test "redirects when the specified user does not exist", %{conn: conn} do
  conn = get conn, user_post_path(conn, :index, -1)
  assert get_flash(conn, :error) == "Invalid user!"
  assert redirected_to(conn) == page_path(conn, :index)
  assert conn.halted
end

Wir haben Folgendes nicht berücksichtigt : user im Vergleich zum Beispiel aus dem Setup- Block , da wir es hier nicht verwenden. Wir überprüfen auch, dass die Verbindung am Ende geschlossen wird.

Und schließlich müssen wir einen Test schreiben, in dem wir versuchen, den Beitrag eines anderen zu bearbeiten.

test "redirects when trying to edit a post for a different user", %{conn: conn, user: user} do
  other_user = User.changeset(%User{}, %{email: "test2@test.com", username: "test2", password: "test", password_confirmation: "test"})
  |> Repo.insert!
  post = build_post(user)
  conn = get conn, user_post_path(conn, :edit, other_user, post)
  assert get_flash(conn, :error) == "You are not authorized to modify that post!"
  assert redirected_to(conn) == page_path(conn, :index)
  assert conn.halted
end

Wir erstellen einen anderen Benutzer, der unser schlechter Benutzer wird, und fügen ihn zu Repo hinzu . Dann versuchen wir, auf die Bearbeitungsaktion für den Beitrag unseres ersten Benutzers zuzugreifen . Dadurch funktioniert der negative Fall unseres authorize_user- Plugins ! Speichern Sie die Datei, führen Sie den Befehl aus mix testund warten Sie auf die Ergebnisse:

.......................................
Finished in 0.4 seconds
39 tests, 0 failures
Randomized with seed 102543

Es geht los! Wir haben viel getan! Aber jetzt haben wir ein funktionierendes (und sichereres) Blog mit Beiträgen, die für Benutzer erstellt wurden. Und wir haben immer noch eine gute Testabdeckung! Es ist Zeit, eine Pause zu machen. Wir werden diese Reihe von Schulungsmaterialien fortsetzen, indem wir die Rolle des Administrators, Kommentare und Markdown-Unterstützung hinzufügen und schließlich mit einem Live-Kommentarsystem in Kanäle einbrechen!

Wichtige Schlussfolgerung des Übersetzers


Ich habe sowohl diesen Artikel als auch die Übersetzung der gesamten Serie hervorragend übersetzt. Was ich jetzt weiter mache. Wenn Ihnen der Artikel selbst oder die Bemühungen, Elixir in RuNet zu popularisieren, gefallen haben, unterstützen Sie den Artikel daher mit Pluspunkten, Kommentaren und Umbauten. Dies ist sowohl für mich persönlich als auch für die gesamte Elixir-Community unglaublich wichtig .

Andere Artikel in der Serie


  1. Eintrag
  2. Einloggen
  3. Rollen hinzufügen
  4. Wir bearbeiten Rollen in Controllern
  5. Wir verbinden ExMachina
  6. Abschriftenunterstützung
  7. Kommentare hinzufügen
  8. Beenden Sie mit Kommentaren
  9. Kanäle
  10. Kanal testen
  11. Fazit


Bei allen Ungenauigkeiten, Fehlern, mangelhafter Übersetzung, bitte schreiben Sie per persönlicher Nachricht, ich werde dies umgehend korrigieren. Ich danke allen im Voraus.

Jetzt auch beliebt: