Tworzenie postów

7

procent przetłumaczone

W tym rozdziale dowiesz się:

  • jak dodawać posty po stronie klienta.
  • jak zaimplementować proste sprawdzenie bepieczeństwa.
  • jak ograniczyć dostęp do formularza dodawania posta.
  • jak użyć metody po stronie serwera dla zwiększenia bezpieczeństwa.
  • Widzieliśmy jak łatwo utworzyć nowy post z konsoli przeglądarki bezpośrednio używając funkcji bazy danych Posts.insert, ale nie możemy oczekiwać, że nasi użytkownicy będą otwierali konsolę przy każdym tworzeniu nowego posta.

    W końcu będziemy musieli zbudować pewien interfejs użytkownika, aby pozwolić użytkownikom dodawać nowe posty z poziomu aplikacji.

    Budowanie strony Nowy Post

    Zaczniemy od zdefiniowania ścieżki dla naszej nowej strony:

    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

    Użyjemy funkcji routera data, aby ustawić kontekst danych szablonu postPage. Pamiętaj, że cokolwiek wstawiliśmy do kontekstu danych, będzie dostępne jako this w obrębie helperów szablonu.

    Dodawanie linka do nagłówka.

    Po zdefiniowaniu ścieżki, możemy dodać link do strony umożliwiającej dodanie posta w nagłówku:

    <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

    Ustawienie ścieżki oznacza, że jeżeli użytkownik przejdzie do URL /submit, Meteor wyświetli szablon postSubmit. Napiszmy zatem ten szablon:

    <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

    Uwaga: jest tu wiele znaczników pochodzących z Bootstrap Twittera. Podczas gdy tylko elementy formularza są istotne, dodatkowe znaczniki pomogą w upiększeniu naszej aplikacji. Powinna wyglądać teraz podobnie do:

    Formularz dodawania posta
    Formularz dodawania posta

    Jest to prosty formularz. Nie musimy się martwić o wykonanie konkretnej akcji, ponieważ będziemy przechwytywali zdarzenie wyślij na forumlarzu i uaktualniali dane za pomocą JavaScript. (Nie ma sensu dostarczać na wszelki wypadek rozwiązania nie-JavaScript, gdy weźmiesz pod uwagę, że aplikacja Meteora jest całkowicie niefunkcjonalna po wyłączeniu obsługi JavaScript).

    Tworzenie postów

    Połączmy handler zdarzeń (ang. event handler) ze zdarzeniem submit formularza. Lepiej użyć zdarzenie submit, niż click na przycisku, jako że to pokryje wszystkie możliwe sposoby wysyłania postów (jak na przykład wciśnięcie przycisku Enter w polu URL).

    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

    Zatwierdź 7-1

    Dodana strona do dodawania postów podlinkowana w nagłówku…

    Powyższa funkcja używa jQuery do parsowania wartości różnych pól formularza i tworzenia nowego obiektu posta z wyników parsowania. Musimy się upewnić, że używamy preventDefault na parametrze event naszego handlera aby upewnić się, że przeglądarka nie będzie kontynuowała i próbowała wysłać formularza.

    Ostatecznie, możemy przekierować uzytkownika to strony nowego posta. Funkcja kolekcji insert zwraca wygenerowany id dla obiektu, który został wstawiony do bazy danych, który użyje funkcja Routera go() aby utworzyć docelowy URL.

    Końcowym rezultatem jest to, że po wciśnieciu przycisku wyślij przez użytkownika, tworzony jest post i użytkownik jest natychmiastowo zabierany do strony z dyskusją na temat tego nowego posta.

    Dodanie kilku środków bezpieczeństwa

    Tworzenie postów ma się dobrze, ale nie chcemy na to pozwolić dowolnemu użytkownikowi: chcemy, aby się zalogowali przed dodaniem posta. Oczywiście możemy zacząć od ukrycia formularza wprowadzania nowego posta dla niezalogowanych użytkowników. Mimo to, użytkownik mógłby ukrycie utworzyć post w konsoli przeglądarki bez zalogowania się, a nie możemy sobie na to pozwolić.

    Na szczęście bezpieczeństwo danych jest wbudowane w podstawy kolekcji Meteora: jest to po prostu domyślnie wyłączone, gdy tworzysz nowy projekt. Pozwala to na łatwy start i budowanie własnej aplikacji zostawiając nudne sprawy na sam koniec.

    Nasza aplikacja już nie potrzebuje tej pomocy, zatem usuńmy pakiet insecure:

    $ meteor remove insecure
    
    Terminal

    Po wykonaniu tej komendy zauważyszm że formularz posta przestanie działać. Dzieje się to, ponieważ bez pakietu insecurewstawianie danych po stronie klienta przestaje być dozwolone. Musimy dodać Metorowi wyraźne zasady pozwalające na wstawianie postów przez klienta lub wstawiać posty po stronie serwera.

    Zezwolenie na wstawianie postów

    Aby rozpocząć, pokażemy jak zezwolić na wstawianie nowych postów po stronie klienta, aby formularz zaczął znowu działać poprawnie. Jak się okaże później, wybierzemy jeszcze inny sposób, ale na teraz poniższy kod wystarczy, aby naprawić działanie aplikacji:

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

    Zatwierdź 7-2

    Usunieto insecure i zezwolono na zapisy do kolekcji posts.

    Wołamy Posts.allow, które przekazuje Meteorowi “jest to zbiór okoliczności dla których klient ma zezwolenie różnych akcji na kolekcji Posts”. W tym przypadku, mówimy “klient może wstawiać posty pod warunkiem posiadania userId”.

    userId użytkownika przeprowadzającego zmianę jest przekazywany do allow i deny (lub zwraca null, jeżeli żaden użytkownik nie jest zalogowany), co jest prawie zawsze pożyteczne. I skoro konta użytkowników są powiązane z głównym modułem Meteora możemy zawsze polegać na prawidłowości userId.

    Zdołaliśmy upewnić się, że musisz być zalogowany, aby móc tworzyć posty. Spróbuj się wylogować i utworzyć posta. Powinieneś zobaczyć w konsoli przeglądarki jak poniżej:

    Insert failed: Access denied
    Insert failed: Access denied

    Jednakże, wciąż musimy dać sobie radę z kilkoma problemami:

    • Wylogowani użytkownicy nadal mają dostę do formularza tworzenia posta
    • Post nie jest związany z użytkownikiem w żaden sposób (i nie ma żadnego kodu po stronie serwera, który by to obsługiwał).
    • Można utworzyć wiele postów, które będą wskazywały na ten sam URL.

    Naprawmy te problemy.

    Zabezpieczanie dostępu do formularza nowego posta

    Zacznijmy od zabezpieczenia przed zobaczeniem formularza dla niezalogowanych użytkowników. Zrobimy to na poziomie routera przez zdefiniowanie funkcji podłączonej do ścieżki (ang. hook).

    Funkcja taka przechwyca proces przekierowania i potencjalnie może zmienić akcję, którą podejmuje router. Możesz myśleć o tym jako o strażniku, który sprawdza twoje dane przez wpuszczeniem Ciebie do środka (lub odmowie dostępu).

    Co potrzebujemy, to sprawdzenie czy użytkownik jest zalogowany, a jeżeli nie jest, wyrenderowanie szablonu accessDenied zamiast oczekiwanego szablonu postSubmit (następnie zatrzymujemy wykonywanie kolejnych funkcji przez router). Zmieńmy zatem router.js tak, jak poniżej:

    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

    Tworzymy również szablon dla strony z odmową dostępu:

    <template name="accessDenied">
      <div class="alert alert-error">Nie masz tu dostępu! Proszę się zalogować.</div>
    </template>
    
    client/views/includes/access_denied.html

    Zatwierdź 7-3

    Odmowa dostępu do strony nowych postów dla niezalogowanyc…

    Jeżeli skierujesz się teraz do http://localhost:3000/submit/ nie będąc zalogowanym, powinieneś zobaczyć:

    Szablon odmowy dostępu
    Szablon odmowy dostępu

    Pozytywnym aspektem funkcji podpiętych pod router jest to, że są reaktywne. Oznacza to, że możemy działać deklaratywnie i nie musimy martwić się o callbacki czy cokolwiek podobnego, gdy użytkownik jest zalogowany. Gdy status zalogowania się użytkownika ulega zmianie, szablony strony routera natychmiastowo zmienia się z accessDenied na postSubmit bez konieczności pisania dodatkowego kodu.

    Zaloguj się, następnie spróbuj odświeżyć stronę. Możesz czasami zobaczyć krótkie mignięcie szablonu odmowy dostępu przed pojawieniem się strony pozwalającej na dodanie posta. Przyczyna tego jest taka, że Meteor rozpoczyna renderowania tak szybko, jak to jest tylko możliwe, zanim skontaktuje się z serwerem i sprawdzi, czy bieżący użytkownik (zapisany w lokalnej bazie danych) nawet istnieje.

    Aby uniknąć tego problemu (który jest częstą klasą problemów, z którymi będziesz musiał się zmierzyć poznając subtelności związanych z opóźnieniem przy komunikacji klient-serwer), wyświetlimy po prostu na ekranie szablon ładowania na krótką chwilę podczas sprawdzania, czy użytkownik ma dostęp.

    W końcu w tym momencie nie wiemy czy użytkownik ma prawidłowe dane do zalogowania i nie możemy pokazać ani accessDenied ani postSubmit aż tego nie sprawdzimy.

    Modyfikujemy zatem naszą funkcję podpiętą do routera aby używała szablonu do ładowania podczas gdy Meteor.logginIn() zwraca true:

    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

    Zatwierdź 7-4

    Show a loading screen while waiting to login.

    Ukrywanie linka

    Najłatwiejszym sposobem aby zapobiec przypadkowy dostęp użytkownikom do tej strony jest ukryć link. Osiągniemy to całkiem łatwo przez:

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

    Zatwierdź 7-5

    Pokazuj link Dodaj post jeżeli uzytkownik jest zalogowany.

    Helper currentUser jest dostarczony przez pakiet accounts i jest odpowiednikiem Meteor.user() dostarczanego przez Handlebars. Ponieważ jest reaktywny, link pojawi się lub nie w zależności od tego, czy się wylogujesz, czy zalogujesz do aplikacji.

    Serwerowe metody Meteora: Większy stopień bezpieczeństwa i abstrakcji

    Zdołaliśmy zabezpieczyć dostęp do strony nowych postów dla niezalogowanych użytkowników, i odmówić takim użytkownim możliwość dodawania postów nawet jeżeli oszukują i używają konsoli przeglądarki. Jest jednak jeszcze kilka rzeczy, o które musimy się zatroszczyć:

    • Dodanie znacznika czasu do posta.
    • Upewnienie się, że ten sam URL nie można dodać więcej, niż jeden raz.
    • Dodanie szczegółów o autorze posta (ID, nazwa użytkownika, itd.).

    Możesz sobie pomyśleć, że wszystko to możemy osiągnąć w funkcji obsługującej zdarzenia submit. Gdybyśmy to zrobili, napotkalibyśmy nową grupę problemów:

    • Jeżeli chodzi o znacznik czasu, musielibyśmy polegać na prawidłowo ustawionym czasie po stronie klienta, co nie zawsze nastąpi
    • Klient nie będzie wiedział o wszystkich URL, które zostały opublikowane na stronie. Będą jedynie wiedzieli o postach, które bieżąco widzą (później dokładnie opiszemy jak to działa), więc nie ma pewnego sposobu na wymuszenie unikalnych adresów URL po stronie klienta.
    • Ostatecznie, chociaż teoretycznie możemy dodawać szczegóły użytkownika po stronie klienta, nie wymuszalibyśmy ich dokładności i zgodności z prawdą i prowadziłoby to do nadużyć związanych z używaniem konsoli przeglądarki.

    Z powodów wymienionych powyżej lepiej jest utrzymywać proste funkcje obsługujące zdarzenia i jeżeli robimy coś więcej niż tylko podstawowa wstawianie lub modyfikacje kolekcji, używać Metod Meteora (metod przez duże ’M’).

    Metoda Meteora jest funkcją serwerową wykonywaną po stronie serwera, ale wywoływaną po stronie klienta. Nie jest to dla nas całkowicie obce – w szczególności funkcje kolekcji insert, update i remove są wszystkie Metodami. Sprawdźmy jak zaimplementować własną.

    Wróćmy do post_submit.js. Zamiast wstawiania bezpośrednio do kolekcji Posts, zawołamy Metodę o nazwie post:

    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

    Funkcja Meteor.call woła Metodę o nazwie określonej w pierwszym parametrze funkcji. Możesz dodać parametry do wywołania funkcji (w tym przypadku obiekt post skonstruowany na podstawie formularza) i ostatecznie dodać callback, który zostanie uruchomiony po zakończeniu Metody. Tutaj po prostu ostrzegamy użytkownika czy wystąpił jakiś problem lub przekierowujemy go nowoutworzonej strony dyskusji konkretnego posta.

    Następnie zdefiniujemy Metodą w pliku collections/posts.js. Usuniemy blok allow() z posts.js, ponieważ Metoda Meteora i tak go ominie. Pamiętaj, że Metody są wykonywane po stronie serwera, więc Meteor uważa, że można im ufać.

    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

    Zatwierdź 7-6

    Uzycie Metody przy dodawaniu posta.

    Metoda ta jest trochę skomplikowana, ale mamy nadzieje że możesz nadążyć.

    Na początkek definiujemy zmienną user i sprawdzamy, czy post z tym samym linkiem już nie istnieje. Następnie sprawdzamy, czy użytkownik jest zalogowany, rzucając błąd (który końcowo będzie wyświetlony w przeglądarce), jeżeli nie jest. Przeprowadzamy także prostą walidację obiektu posta, aby upewnić się, że post zawiera tytuł.

    Następnie, jeżeli istnieje już post z tym samym URL, rzucamy błąd 302 (który oznacza przekierowanie), przekazaując użytkownikowi, że powinien sprawdzić poprzednio tworzony post.

    Klasa Error Meteora ma trzy parametry. Pierwszy, (error) będzie numerycznym kodem 302, drugi (reason) zawiera wyjaśnienie zrozumiałe dla człowieka i trzeci (details) może być jakąkolwiek użyteczną informacją.

    W naszym przypadku, użyjemy trzeci argument aby przekazać ID posta, który właśnie znaleźliśmy. Uwaga: użyjemy tego później aby przekierować użytkownika do jeszcze nieistniejącego posta.

    Jeżeli wszystkie kroki są sprawdzone i przechodzą poprawnie, bierzemy pola, które chcemy wstawić (upewniając się, że użytkownik wołający tą Metodę w konsoli przeglądarki nie może dodać fałszywych danych do bazy) i dodajemy pewne informacje o uzytkowniku dodającym posta – jak również bieżący czas – do posta.

    Ostatecznie, wstawiamy post do bazy danych i zwracamy nowy id użytkownikowi.

    Sortowanie postów

    Teraz skoro mamy dostęp do czasu wstawienia wszystkich postów, ma sens upewnienie się, że są posortowane ze względu na ten atrybut. Aby to uczynić, możemy użyć operatora Mongo sort, który oczekuje obiektu składającego się z kluczy po których posortować i znaku oznaczającego czy sortowanie ma być wykonane rosnąco czy malejąco.

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

    Zatwierdź 7-7

    Sortowanie postów po znaczniku czasu.

    Zabrało to trochę pracy, ale wreszcie mamy interfejs użytkownika, który pozwala bezpiecznie wstawiać nowe dane do naszej aplikacji!

    Jakakolwiek aplikacja, która pozwala użytkownikom na tworzenie nowych treści musi dać im także sposób na ich późniejszą edycję lub usuwania. Poświęcony jest temu rozdział Edytowanie Postów.