Creare i post

7

Percentuale Tradotta

In questo capitolo imparerai:

  • Impara ad inviare un post client-side.
  • Implementare un semplice controllo di sicurezza.
  • Limitare l'accesso al form di invio del post.
  • Imparare ad usare i metodi server-side per aggiungere sicurezza.
  • Abbiamo visto come è facile creare dei post nella console, usando la richiesta per il database ‘Post.insert’, ma non possiamo aspettarci che gli utenti aprano la console per creare un nuovo post.

    Eventualmente, dovremo costruire un'interfaccia utente che consenta agli utenti di inviare nuove storie sulla nostra applicazione.

    Costruire la pagina 'Nuovo Post’

    Iniziamo col definire un percorso ('route’) per la nuova pagina:

    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'
      });
    });
    
    Router.onBeforeAction('loading');
    
    lib/router.js

    Stiamo usando la funzione 'data’ per impostare il contesto dati del template 'postPage’. Ricorda che qualsiasi cosa inseriamo nel contesto 'data’, sarà disponibile come 'this’ negli helper del template.

    Aggiungere un link nella testata

    Adesso, avendo definito quel percorso, possiamo aggiungere un link nella nostra testata che colleghi alla pagina d'invio:

    <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

    Impostare il nostro percorso significa che se l'utente accede all'URL ’/submit’, Meteor mostrerà il template 'postSubmit’. Scriviamo quindi il template:

    <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=""></textarea>
            </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

    Nota: qui c'è molta markup, ma è dovuto al fatto che stiamo utilizzando Twitter Bootstrap. Anche se l'unica cosa essenziale sono gli elementi del modulo, la markup aggiuntiva ci aiuterà a rendere la nostra applicazione più presentabile. Adesso dovrebbe apparire così:

    The post submit form
    The post submit form

    Questo è un modulo semplice. Non dobbiamo preoccuparci di aggiungerci una funzione perché intercetteremo eventi d'invio nel modulo stesso ed aggiorneremo i dati via JavaScript. (Non ha senso procurare un non-JS fallback se consideri che un'applicazione Meteor non funzionerebbe senza JavaScript).

    Creare i Post

    Fissiamo un event handler all'evento 'submit’ del modulo. È preferibile intercettare questo evento, anziché l'evento 'click’ sul bottone, perché coprirà ogni possibile modo di invio, premendo 'invio’ nel form, ad esempio.

    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.

    Questa funzione usa jQuery per analizzare i valori dei vari campi modulo e riempire l'oggetto post con i risultati. Dobbiamo accertarci che 'preventDefault’ sia impostato sull'argomento 'event’ del nostro gestore (handler) così da evitare che il browser invii il modulo.

    Finalmento possiamo fare rotta verso la pagina del nostro nuovo post. La funzione 'insert()’ su una collezione ritorna l’'id’ generato per l'oggeto che è stato inserito nel database e verrà utilizzata dalla funzione 'go()’ del Router per creare un URL a cui potremo accedere.

    Il risultato è che l'utente preme invio, il post viene creato e l'utente viene diretto istantaneamente alla pagina di discussione relativa al nuovo post.

    Aggiungere sicurezza

    Creare i post va bene, ma non possiamo permettere che un qualsiasi visitatore lo faccia: vogliamo che siano connessi. Certo, potremmo iniziare col nascondere i moduli per un nuovo post agli utenti non connessi. Tuttavia, l'utente potrebbe presubimilmente creare un post nella console del browser senza essersi connesso e questo non possiamo permetterlo.

    Fortunatamente la sicurezza dei dati è intrinseca alle collezioni di Meteor; è solo che è disattivata per impostazione predefinita quando crei un nuovo progetto. Ciò ti permette di fare i primi passi con facilità e di aggiungere strati alla tua applicazione lasciando gli aspetti più noiosi a dopo.

    Adesso, la nostra applicazione non ha più bisogno delle rotelle di addestramento quindi leviamole! Rimuoviamo il pacchetto 'insecure’:

    $ meteor remove insecure
    
    Terminal

    Fatto questo, vedrai che il modulo del post non funzionerà più. Il motivo è che senza il pacchetto 'insecure’, non sono più permessi insert, in nessuna collezione, lato client. O diamo degli ordini precisi a Meteor per quando permettere all'utente di inserire i post o li inseriamo lato server con dei metodi specifici, che vedremo in seguito.

    Allowing Post Inserts

    Per iniziare, vi dimostreremo come permettere inserimenti dal client per far nuovamente funzionare il nostro modulo. Come scopriremo più avanti, la tecnica utilizzata alla fine sarà un'altra, ma per il momento, quanto segue, farà funzionare le cose facilmente:

    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.

    'Posts.allow’ comunica a Meteor che “queste sono le circostanze sotto le quali i client possono agire sulla collezione 'Posts’”. In questo caso specifico stiamo dicendo “i client possono inserire i post purché abbiano un 'userId’ ”.

    L’'userId’ dell'utente che sta apponendo la modifica viene passata alle chiamate 'allow’ e 'deny’ (o ritorna 'null’ se nessun utente è connesso) ed è quasi sempre utile. Dal momento che gli account degli utenti sono legati al nucleo di Meteor, possiamo confidare nel fatto che 'userId’ sia sempre corretta.

    Siamo riusciti a garantire la necessità di essere connessi per creare un post. Proviamo a creare un post dopo aver effetuato un log out; dovremmo vedere questo:

    Insert failed: Access denied
    Insert failed: Access denied

    Tuttavia, abbiamo ancora un paio di problemi da rsolvere:

    • Utenti non connessi possono ancora accedere al modulo per creare post.
    • Il post non è legato all'utente in nessun modo (e non c'è nemmeno codice sul server per farlo applicare)
    • È possibile creare post multipli che condividono lo stesso URL

    Risolviamo questi problemi:

    Garantire Accesso Al Modulo Per Il Nuovo Post

    Iniziamo con l'impedire agli utenti non connessi di vedere il modulo d'invio del post. Per far questo accederemo al livello del router, definendo un route hook.

    Un hook (gancio) intercetta il processo di routing e potenzialmente cambia l'azione che esegue il router. Puoi pensarla come una guardia di sicurezza che controlla i tuoi documenti prima di farti passare (o respingerti).

    Dobbiamo controllare che l'utente sia connesso e se non lo fosse, attivare il template 'accesDenied’ invece del previsto 'postSubmit’ (poi fermiamo il router dal fare nient'altro). Modifichiamo router.js così:

    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'
      });
    });
    
    
    var requireLogin = function(pause) {
      if (! Meteor.user()) {
        this.render('accessDenied');
        pause();
      }
    }
    
    Router.onBeforeAction('loading');
    Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
    
    lib/router.js

    Creiamo anche il template per la pagina 'accesso negato’:

    <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.

    Se adesso vai su http://localhost:3000/submit/ senza essere connesso, dovresti vedere questo:

    The access denied template
    The access denied template

    Il bello dei routing hooks (ganci) è che sono reattivi. Ciò significa che possiamo essere dichiarativi e non dobbiamo preoccuparci di callbacks o simili quando l'utente si connette. Quando lo stato di registrazione dell'utente cambia, il template della pagina del Router cambia istantaneamente a sua volta da 'accessDenied’ a 'postSubmit’ senza dover scrivere una linea di codice per gestirlo.

    Connettiti e prova a ricaricare la pagina. Può capitare di veder lampeggiare brevemente il template di accesso negato prima che la pagina d'invio post appaia. Ciò accade perché Meteor esegue il rendering dei template appena può, ancor prima di parlare con il server e verificare che l'utente (salvato nella memoria locale del browser) esista.

    Per evitare questo problema (un problema molto comune che incontrerai spesso quando affronterai le complessità di latenza tra client e server), mostreremo una pagina di caricamento durante il breve istante necessario a verificare se l'utente ha accesso oppure no.

    Dopo tutto a questo punto non sappiamo ancora se l'utente ha le corrette credenziali di registrazione e non possiamo mostrare i template di 'accesDenied’ o 'postSubmit’ prima di averlo confermato.

    Modifichiamo quindi il nostro hook al fine di utilizzare il template di caricamento mentre 'Meteor.logginIn()’ è 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(pause) {
      if (! Meteor.user()) {
        if (Meteor.loggingIn())
          this.render(this.loadingTemplate);
        else
          this.render('accessDenied');
    
        pause();
      }
    }
    
    Router.onBeforeAction('loading');
    Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
    
    lib/router.js

    Commit 7-4

    Show a loading screen while waiting to login.

    Nascondere il Link

    Il modo più semplice per prevenire che l'utente non connesso possa accedere a questa pagina per sbaglio è di nascondere il link. È molto facile:

    <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.

    L'helper 'currentUser’ viene fornito dal pacchetto 'accounts’ ed è l'equivalente di 'Meteor.user()’ per Spacebars. Dal momento che è reattivo, il Link appare o scompare al momento della connessione o disconnessione dall'applicazione.

    Meteor Method: Miglior sicurezza e astrazione

    Siamo riusciti a garantire accesso alla pagina del nuovo post per utenti non connessi e negarli dal creare post nuovi anche se provassero ad imbrogliare usando la console. Detto questo, ci sono però ancora un paio di cose da aggiustare:

    • Data e ora per i post.
    • Garantire che lo stesso URL non sia postato più di una volta.
    • Aggiungere informazioni sull'autore del post (ID, nome utente, eccetera).

    Potresti pensare di poter eseguire tutto questo all'interno del gestore di eventi 'submit’. In realtà, incorreremmo in diversi problemi di varia natura.

    • Per data e ora, dovremmo affidarci all'accuratezza dell'orologio nel computer dell'utente, ma non è sempre attendibile.
    • I clients non saranno sempre al corrente di tutti gli URL postati sul sito. Sapranno solo dei post che sono visibili in quel momento (più tardi vedremo come funziona esattamente) quindi non c'è modo di forzare l'unicità del'URL lato client.
    • Infine, anche se potessimo aggiungere informazioni sull'utente lato client, non potremmo imporne l'accuratezza perché potrebbe lasciare l'applicazione suscettibile ad attacchi provenienti dalla console del browser.

    Per questi motivi sopra indicati, è consigliabile mantenere i gestori di eventi semplici e se vogliamo eseguire inserzioni più sofisticate o aggiornare collezioni, usiamo un Method.

    Un Meteor Method è una funzione server-side chiamata dal client. Non ci sono totalmente sconosciuti, infatti, dietro le quinte, l’'insert’ di 'Collection’ e le funzioni 'update’, 'remove’ sono tutti Methods. Vediamo come creare il nostro.

    Torniamo a 'post_submit.js’. Invece che inserirlo direttamente nella collezione 'Posts’, richiamiamo un Method chiamato '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

    La funzione 'Meteor.call’ richiama un Method nominato dal suo primo argomento. Puoi fornire argomenti alla chiamata (in questo caso l'oggetto 'post’ costruito dal modulo) ed infine allegare un callback che viene chiamato (execute) quando il Method del server è concluso. Qui stiamo semplicemente allertando l'utente nel caso ci fosse un problema oppure ridirigerlo alla pagina di discussione del post appena creato nel caso non ci fosse.

    Definiamo poi il Method nel file 'collections/posts.js’. Rimuoviamo il blocco 'allow()’ da 'posts.js’ considerando che Metheor Methods li bypassano comunque. Ricordiamoci che i Methods sono eseguiti sul server quindi Meteor assume che siano attendibili.

    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.

    Questo Method è un po’ complicato ma dovresti riuscire a comprenderlo.

    Prima di tutto definiamo la variabile 'user’ e controlliamo se c'è un altro post con lo stesso link. Verifichiamo che l'utente sia connesso e nel caso non lo fosse lanciamo un'errore (allertato con 'alert’). Eseguiamo anche una piccola convalida dell'oggetto post per assicurarsi che i nostri post abbiano un titolo.

    Dopo, se ci fosse un altro post con lo stesso link, lanciamo un errore '302’ (che reindirizza) informando l'utente che dovrebbero andare a guardare il post creato precedentemente.

    La classe 'Error’ di Meteor ha tre argomenti. Il primo ('error’) sarà il codice numerico '302’, il secondo ('reason’) è una breve spiegazione leggibile dell'errore e l'ultimo ('details’) può essere ogni utile informazione aggiuntiva.

    Nel nostro caso, useremo il terzo argomento per passare l'ID del post che abbiamo appena trovato. Spoiler alert: lo useremo più tardi per reindirizzare l'utente al post preesistente.

    Se tutte queste verifiche passano, prendiamo i campi che vogliamo inserire ed includiamo alcune informazioni sull'utente che sta pubblicando – inclusa l'ora attuale – nel post.

    Infine, inseriamo il post e diamo l’'id’ del nuovo post all'utente.

    Mettere i post in ordine

    Ora che abbiamo una data per ogni post, ha senso garantire che siano messi in ordine con questo attributo. Per fare ciò, possiamo usare l'operatore 'sort’ di Mongo, che si aspetta di ricevere un oggetto composto dalle chiavi usate per riordinare ed un simbolo che indichi se siano ascendenti o discendenti.

    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.

    C'è voluto un po’ di lavoro ma abbiamo finalmente un'interfaccia utente che permetterà agli utenti di inserire del contenuto nella nostra applicazione!

    Detto questo, ogni applicazione che permetta all'utente di creare contenuti deve anche dargli modo di modificarli e cancellarli. Questo sarà oggetto del prossimo capitolo 'Modificare i Post’.