Beiträge erstellen

7

Zu ...% übersetzt

In diesem Kapitel wirst du:

  • Lerne wie man Beitragsdaten client-seitig versendet.
  • Implementiere einen Sicherheitscheck.
  • Begrenze den Zugriff zum Formular.
  • Lerne wie man eine serverseitige Methode für zusätzliche Sicherheit nutzt.
  • Wir haben nun gesehen wie einfach es ist, Beiträge über die Konsole mit dem Datenbankaufruf Posts.insert anzulegen. Aber wir können natürlich nicht erwarten, dass unsere Benutzer die Konsole benutzen.

    Also brauchen wir ein Benutzer-Interface, um unseren Benutzern die Möglichkeit zu geben, neue Beiträge anzulegen.

    Aufbau der Seite zum Anlegen eines Beitrages

    Wir fangen an, indem wir eine neue Route für eine Seite definieren:

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      waitOn: function() { return Meteor.subscribe('posts'); }
    });
    
    Router.map(function() {
      this.route('postsList', {path: '/'});
    
      this.route('postPage', {
        path: '/posts/:_id',
        data: function() { return Posts.findOne(this.params._id); }
      });
    
      this.route('postSubmit', {
        path: '/submit'
      });
    });
    
    lib/router.js

    Wir benutzen die Funktion data des Routers, um den Kontext postPage des Templates zu setzen. Zur Erinnerung: Was immer wir in den Kontext legen, wird als this innerhalb des Template-Helpers verfügbar sein.

    Hinzufügen eines Links zur Überschrift

    Da die Route nun angelegt ist, können wir nun einen Link darauf in der Navigation unserer Seite hinzufügen:

    <template name="header">
      <header class="navbar">
        <div class="navbar-inner">
          <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </a>
          <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
          <div class="nav-collapse collapse">
            <ul class="nav">
              <li><a href="{{pathFor 'postSubmit'}}">New</a></li>
            </ul>
            <ul class="nav pull-right">
              <li>{{loginButtons}}</li>
            </ul>
          </div>
        </div>
      </header>
    </template>
    
    client/views/includes/header.html

    Das Anlegen der Route bedeutet, dass wenn der Benutzer die URL /submit aufruft, Meteor das Template postsubmit anzeigen wird. Wir legen dieses Template nun an:

    <template name="postSubmit">
      <form class="main">
        <div class="control-group">
            <label class="control-label" for="url">URL</label>
            <div class="controls">
                <input name="url" type="text" value="" placeholder="Your URL"/>
            </div>
        </div>
    
        <div class="control-group">
            <label class="control-label" for="title">Title</label>
            <div class="controls">
                <input name="title" type="text" value="" placeholder="Name your post"/>
            </div>
        </div>
    
        <div class="control-group">
            <label class="control-label" for="message">Message</label>
            <div class="controls">
                <textarea name="message" type="text" value=""/>
            </div>
        </div> 
    
        <div class="control-group">
            <div class="controls">
                <input type="submit" value="Submit" class="btn btn-primary"/>
            </div>
        </div>
      </form>
    </template>
    
    
    client/views/posts/post_submit.html

    Bemerkung: Das ist einiges an Markup. Das wird durch die Benutzung von Twitters Bootstrap verursacht. Eigentlich sind nur die Formularelemente wichtig. Das restliche Markup hilft aber, unser App ein wenig besser aussehen zu lassen. Das gerenderte Template sollte nun wie folgt aussehen:

    Das Formular zum Anlegen von Beiträgen
    Das Formular zum Anlegen von Beiträgen

    Das ist ein einfaches Formular. Wir brauchen uns über die ´Action´ des Formulars keine Gedanken zu machen. Den Submit-Event des Formulars werden wir ohnehin abfangen und die Daten per JavaScript aktualisieren. (Es macht auch keinen Sinn einen Fallback für nicht vorhandenes JavaScript im Browser einzurichten, da eine Meteor-App ohne JavaScript nicht funktioniert.)

    Das Anlegen von Beiträgen

    Wir verbinden nun das Formular mit einem Event-Handler. Am besten verwenden wir dafür den submit-Event (anstatt z.B. den Click-Event des Buttons). Das hat den Vorteil, dass alle möglichen Wege für das Absenden des Formulars abgedeckt werden (wie zB. das Benutzen der Return-Taste im URL-Feld).

    Template.postSubmit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var post = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val(),
          message: $(e.target).find('[name=message]').val()
        }
    
        post._id = Posts.insert(post);
        Router.go('postPage', post);
      }
    });
    
    client/views/posts/post_submit.js

    Commit 7-1

    Added a submit post page and linked to it in the header.

    Diese Funktion verwendet jQuery, um die Werte aus den verschiedenen Formularfeldern zu entnehmen und ein neues Beitrags-Objekt anzulegen. Wir müssen dabei beachten, dass die Methode preventDefault() auf dem Parameter event in unserem Handler aufgerufen wird. Ansonsten würde der Browser versuchen, dass Formular abzusenden.

    Am Ende können wir per Router auf die Seite des neuen Beitrag umleiten. Die Funktion insert() einer Collection gibt die erzeugte id des Objektes zurück, welche in die Datenbank eingefügt wurde. Die Funktion go() des Routers erzeugt uns die URL aus ihren Parametern.

    Das Resultat ist, dass wenn der Benutzer auf den Absende-Button drückt, ein Beitrag angelegt wird und der Benutzer auf die Diskussionsseite für diesen Beitrag gelangt.

    Etwas mehr Sicherheit

    Das Anlegen von Beiträgen funktioniert soweit. Aber wir wollen nicht, dass jeder x-beliebige Besucher der Webseite Beiträge anlegen kann. Wir möchten, dass Benutzer dafür eingeloggt sein müssen. Sicherlich könnten wir das Formular zum Anlegen von neuen Beiträgen vor ausgeloggten Benutzern verstecken. Aber ein findiger Benutzer könnte die Beiträge immer noch auf der Konsole anlegen. Das wollen wir natürlich nicht.

    Datensicherheit ist glücklicherweise direkt in den Meteor Collections eingebaut. Sie ist nur standardmässig deaktiviert, wenn ein neues Projekt angelegt wurde. Das erlaubt es einfach mit dem Aufbau der App anzufangen und den langweiligen Teil später zu erledigen.

    Diese Stützräder braucht unsere App jetzt nicht mehr. Wir nehmen sie nun weg. Wir entfernen das Package insecure:

    $ meteor remove insecure
    
    Terminal

    Danach wirst du feststellen, das das Formular nicht mehr funktioniert. Ohne das Package insecure sind client-seitige Einfügeoperationen in Collections nicht mehr erlaubt. Wir müssen entweder explizite Regeln anlegen, um Meteor mitzuteilen, dass es OK ist auf dem Client Objekte anzulegen oder wir müssen das Einfügen auf die Server-Seite verlegen.

    Das Einfügen von Beiträgen wieder erlauben

    Wir zeigen nun, wie man das client-seitige Anlegen von Beiträgen erlaubt. Damit wird das Formular wieder funktionsfähig. Später werden wir zwar eine andere Technik benutzen, aber im Moment ist der folgende Code der einfachere Weg:

    Posts = new Meteor.Collection('posts');
    
    Posts.allow({
      insert: function(userId, doc) {
        // only allow posting if you are logged in
        return !! userId;
      }
    });
    
    collections/posts.js

    Commit 7-2

    Removed insecure, and allowed certain writes to posts.

    Wir rufen die Methode allow() auf der Posts-Collection auf. Diese teilt Meteor mit unter welchen Umständen Clients einen Beitrag einfügen dürfen. In diesem Fall sagen wir “Clients dürfen neue Beiträge anlegen, wenn sie eine userId besitzen”.

    Die userId des Benutzers, der das Anlegen des Beitrags vornimmt, wird an die Aufrufe von allow und deny weitergeleitet (oder liefert null, wenn kein Benutzer eingeloggt ist). Da die Benutzer-Accounts im Kern von Meteor verankert sind, können wir uns darauf verlassen, dass die userId immer korrekt gesetzt ist.

    Wir haben also sichergestellt, dass der Benutzer immer eingeloggt sein muss, um einen Beitrag anzulegen. Versuch einmal, dich auszuloggen und einen Beitrag anzulegen. Du solltest Folgendes in der Konsole sehen:

    Insert failed: Access denied
    Insert failed: Access denied

    Aber wir sind noch nicht fertig:

    • Ausgeloggte Benutzer können immer noch das Anlegeformular sehen.
    • Der Beitrag ist noch nicht mit dem Benutzer verknüpft (und es gibt keinen Code auf dem Server um dies sicherzustellen)
    • Mehrere Beiträge können angelegt werden, die die selbe URL enthalten.

    Versuchen wir diese Probleme anzugehen.

    Absichern des Zugangs zum Anlegeformular eines Beitrages

    Wir fangen an, indem wir ausgeloggte Benutzer daran hindern, das Anlegeformular aufzurufen. Wir werden dies im Router implementieren, in dem wir einen route hook definieren.

    Ein Hook fängt den Routing-Prozess ab und kann die Aktionen, die ein Router vornimmt verändern. Du kannst es als eine Art Wachmann betrachten, der deinen Ausweis überprüft, bevor er Dich hereinlässt (oder abweist).

    Dazu müssen wir überprüfen, ob ein Benutzer eingeloggt ist. Wenn nicht muss anstelle des Templates postSubmit das Template accessDenied ausgegeben werden. Der Router wird dazu wie folgt angepasst:

    Router.configure({
      layoutTemplate: 'layout'
    });
    
    Router.map(function() {
      this.route('postsList', {path: '/'});
    
      this.route('postPage', {
        path: '/posts/:_id',
        data: function() { return Posts.findOne(this.params._id); }
      });
    
      this.route('postSubmit', {
        path: '/submit'
      });
    });
    
    var requireLogin = function() {
      if (! Meteor.user()) {
        this.render('accessDenied');
        this.stop();
      }
    }
    
    Router.before(requireLogin, {only: 'postSubmit'});
    
    lib/router.js

    Wir legen jetzt ein Template für den verweigerten Zugriff an:

    <template name="accessDenied">
      <div class="alert alert-error">You can't get here! Please log in.</div>
    </template>
    
    client/views/includes/access_denied.html

    Commit 7-3

    Denied access to new posts page when not logged in.

    Wenn du nun http://localhost:3000/submit/ aufrufst ohne eingeloggt zu sein, solltest du Folgendes sehen:

    Das Template für den verweigerten Zugriff
    Das Template für den verweigerten Zugriff

    Was Routing-Hooks richtig nett macht ist, dass sie reaktiv sind. Das bedeutet, wir können deklarativ arbeiten und müssen uns nicht um Callbacks oder ähnliches kümmern, wenn der Benutzer sich einloggt. Wenn sich der Login-Zustand eines Benutzers ändert, wird das Seitentemplate sofort von accessDenied zu postSubmit geändert - ohne das wir explizit Code dafür schreiben müssen.

    Log dich ein und versuch dann die Seite neu zu laden. Du wirst eventuell das Template “Zugriff verweigert” für einen kurzen Moment aufflackern sehen, bevor das Anlageformular erscheint. Der Grund dafür ist, dass Meteor Templates so früh wie möglich rendert. Es kann sein, dass dies früher geschieht als der Server mitteilen kann, ob der Benutzer eingeloggt ist. (Der Zustand wird übrigens im LocalStorage des Browsers zwischengespeichert.)

    Um dieses Problem zu verhindern (es handelt sich um ein Problem, welches Du häufiger sehen wirst, wenn Du Dich mit den Eigenheiten von Latenz zwischen Server und Client beschäftigst), werden wir für eine kurze Zeit einen Ladebildschirm anzeigen und warten bis feststeht, ob der Benutzer eingeloggt ist oder nicht.

    Dies ist notwendig, da wir ohne den Server nicht entscheiden können, ob der Benutzer eine korrekte Authorisierung vorgenommen hat oder nicht. Solange können wir weder das Template accessDenied noch das Template postSubmit anzeigen.

    Wir ändern also unseren Hook, um unser Lade-Template anzuzeigen. Dies bleibt solange Meteor.loggingIn() den Wert true zurückliefert:

    Router.map(function() {
      this.route('postsList', {path: '/'});
    
      this.route('postPage', {
        path: '/posts/:_id',
        data: function() { return Posts.findOne(this.params._id); }
      });
    
      this.route('postSubmit', {
        path: '/submit'
      });
    });
    
    var requireLogin = function() {
      if (! Meteor.user()) {
        if (Meteor.loggingIn())
          this.render(this.loadingTemplate);
        else
          this.render('accessDenied');
    
        this.stop();
      }
    }
    
    Router.before(requireLogin, {only: 'postSubmit'});
    
    lib/router.js

    Commit 7-4

    Show a loading screen while waiting to login.

    Links verstecken

    Die einfachste Möglichkeit ausgeloggte Benutzer daran zu hindern, versehentlich das Anlegeformular zu erreichen, ist es den Link zu verstecken. Das geht ziemlich einfach:

    <ul class="nav">
      {{#if currentUser}}<li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>{{/if}}
    </ul>
    
    client/views/includes/header.html

    Commit 7-5

    Only show submit post link if logged in.

    Der Helper currentUser wird durch das Package accounts und sein Handlebar-Äquivalent Meteor.user() zur Verfügung gestellt. Weil der Helper ebenfalls reaktiv ist, wird der Link angezeigt, sobald Du Dich einloggst. Wenn Du Dich ausloggst verschwindet er automatisch.

    Die Meteor-Methode: Bessere Abstraktion und Sicherheit

    Wir haben es geschafft, den Zugriff auf das Anlegeformular für ausgeloggte Benutzer zu beschränken. Auch ist es jetzt für sie nicht mehr möglich, auf der Konsole Beiträge anzulegen. Aber ein paar Dinge bleiben noch übrig, um die wir uns kümmern müssen:

    • Zeitstempel für die Beiträge
    • Verhindern von zwei Beiträgen, die die selbe URL benutzen
    • Details über den Author des Beitrags (id, Benutzername, usw.)

    Du wirst dir vielleicht denken, dass wir dass alles im Event-Handler submit erledigen können. Realistisch betrachtet, würde das allerdings zu einer Menge Probleme führen.

    • Für den Timestamp müssten wir uns darauf verlassen, dass der Computer des Benutzers die korrekte Uhrzeit hat. Das ist leider nicht immer der Fall.
    • Clients kennen nur einen Teil der benutzten URLS. Nämlich die, deren Beiträge sie sehen können (wir werden uns nachher anschauen, wie das genau funktioniert). Also gibt es keinen zuverlässigen Weg, client-seitig die Eindeutigkeit der URL zu überprüfen.
    • Schließlich, obwohl wir die Benutzerdaten client-seitig hinzufügen könnten, können wir nicht gewährleisten, dass alle Angaben stimmen. Das könnte von findigen Personen in der Browser-Konsole missbraucht werden.

    Aus all diesen Gründen ist es besser, wenn wir unsere Event-Handler einfach halten - und wenn wir mehr als einfachste Einfüge- oder Update-Operationen benötigen, benutzen wir eine Methode

    Eine Meteor-Methode ist eine serverseitige Funktion, die vom Client aufgerufen wird. Genau genommen kennen wir sie schon – hinter den Kulissen sind die insert, update und remove-Funktionen der Collection allesamt Methoden. Schauen wir uns mal an, wie wir selber welche erzeugen können.

    Lass uns dazu noch mal die Datei post_submit.js anschauen. Anstelle direkt in die Collection Posts einzufügen, rufen wir nun eine Methode namens post auf:

    Template.postSubmit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var post = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val(),
          message: $(e.target).find('[name=message]').val()
        }
    
        Meteor.call('post', post, function(error, id) {
          if (error)
            return alert(error.reason);
    
          Router.go('postPage', {_id: id});
        });
      }
    });
    
    client/views/posts/post_submit.js

    Die Funktion Meteor.call ruft eine Methode auf, die durch ihren ersten Parameter spezifiziert wird. Du kannst weitere Parameter (in diesem Fall das Objekt post, welches wir aus dem Formular zusammengebaut haben) und einen Callback übergeben. Der Callback wird ausgeführt, wenn die Methode auf dem Server abgearbeitet ist. In diesem Fall geben wir Rückmeldung an den Benutzer, ob Probleme aufgetreten sind oder leiten ihn auf die Diskussionsseite für den Beitrag weiter.

    Danach definieren wir die neue Methode in der Datei collections/posts.js. Wir entfernen den Block allow(), weil Meteor-Methoden diesen ohnehin umgehen. Du erinnerst dich vielleicht: Methoden werden auf dem Server ausgeführt. Meteor nimmt an, dass diese deshalb vertrauenswürdig sind.

    Posts = new Meteor.Collection('posts');
    
    Meteor.methods({
      post: function(postAttributes) {
        var user = Meteor.user(),
          postWithSameLink = Posts.findOne({url: postAttributes.url});
    
        // ensure the user is logged in
        if (!user)
          throw new Meteor.Error(401, "You need to login to post new stories");
    
        // ensure the post has a title
        if (!postAttributes.title)
          throw new Meteor.Error(422, 'Please fill in a headline');
    
        // check that there are no previous posts with the same link
        if (postAttributes.url && postWithSameLink) {
          throw new Meteor.Error(302, 
            'This link has already been posted', 
            postWithSameLink._id);
        }
    
        // pick out the whitelisted keys
        var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
          userId: user._id, 
          author: user.username, 
          submitted: new Date().getTime()
        });
    
        var postId = Posts.insert(post);
    
        return postId;
      }
    });
    
    collections/posts.js

    Commit 7-6

    Use a method to submit the post.

    Diese Methode ist ein wenig komplizierter, aber wir hoffen du kannst uns folgen.

    Zuerst definieren wir die Variable user. Wir überprüfen, ob ein Beitrag mit dem selben Link schon existiert. Dann wird geschaut, ob der Benutzer eingeloggt ist. Ein Fehler wird geworfen, wenn das nicht der Fall ist (der Fehler kann später im Browser angezeigt werden). Wir validieren danach den Beitrag auf einfache Weise, um sicher zu gehen, dass der Beitrag einen Titel hat.

    Als Nächstes, falls ein weiterer Beitrag mit der selben URL existiert, werfen wir einen Fehler 302 (der einen Redirect entspricht). Dadurch können wir dem Benutzer mitteilen, dass er sich den vorherigen Beitrag anschauen soll.

    Meteors Klasse Error nimmt drei Parameter auf. Der erste (error) ist in diesem Fall der numerische Code 302. Der Zweite (reason) ist eine kurze menschenlesbare Fassung des Fehlers. Der dritte (details) kann dazu genutzt werden hilfreiche zusätzliche Information weiterzugeben.

    In unserem Fall, benutzen wir den dritten Parameter, um die ID des bereits existierenden Beitrags weiterzureichen. Spoiler: Wir werden dies später benutzen, um den Benutzer auf die Seite des vorherigen Beitrags weiterzuleiten.

    Wenn all diese Überprüfungen erfolgreich waren, übernehmen wir lediglich die Felder des Objektes, die wir einfügen wollen (um zu vermeiden, dass der Benutzer weitere Felder in unsere Datenbank einfügen kann, z.B. in dem er die Konsole verwendet). Ausserdem fügen wir zusätzliche Information über den Benutzer, sowie den Zeitpunkt des Anlegens in den Beitrag ein.

    Als Letztes fügen wir den Beitrag ein und geben die ID des erzeugten Objekts zurück.

    Sortieren von Beiträgen

    Jetzt, da alle Beiträge über ein Datum verfügen, macht es Sinn danach zu sortieren. Um das zu erreichen, können wir Mongos Operator sort verwenden. Dieser erwartet, dass ein Objekt aus Schlüsseln besteht, nach deren Werten sortiert werden kann. Zusätzlich gibt ein Vorzeichen an, ob wir aufsteigend oder absteigend sortieren.

    Template.postsList.helpers({
      posts: function() {
        return Posts.find({}, {sort: {submitted: -1}});
      }
    });
    
    client/views/posts/posts_list.js

    Commit 7-7

    Sort posts by submitted timestamp.

    Es hat ein wenig Arbeit erfordert, aber wir haben nun ein Benutzerinterface, welches es unseren Benutzer erlaubt, auf abgesicherte Art und Weise Daten in unsere App einzugeben.

    Aber jede App, die den Benutzer Inhalte anlegen lässt, muss auch einen Weg anbieten diese zu ändern und zu löschen. Das werden wir im Kapitel “Ändern von Beiträgen” behandeln.