Creating Posts

7

Percentage Translated

In this chapter, you will:

  • Learn how to submit a post client-side.
  • Implement a simple security check.
  • Restrict access to the post submit form.
  • Learn to use a server-side Method for added security.
  • ////

    ////

    Building The New Post Page

    ////

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

    Adding A Link To The Header

    ////

    <template name="header">
      <nav class="navbar navbar-default" role="navigation">
        <div class="container-fluid">
          <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navigation">
              <span class="sr-only">Toggle navigation</span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
          </div>
          <div class="collapse navbar-collapse" id="navigation">
            <ul class="nav navbar-nav">
              <li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>
            </ul>
            <ul class="nav navbar-nav navbar-right">
              {{> loginButtons}}
            </ul>
          </div>
        </div>
      </nav>
    </template>
    
    client/templates/includes/header.html

    ////

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

    ////

    The post submit form
    The post submit form

    ////

    Creating Posts

    ////

    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()
        };
    
        post._id = Posts.insert(post);
        Router.go('postPage', post);
      }
    });
    
    client/templates/posts/post_submit.js

    Commit 7-1

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

    ////

    ////

    ////

    Adding Some Security

    ////

    ////

    ////

    meteor remove insecure
    
    Terminal

    ////

    ////

    Allowing Post Inserts

    ////

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

    Commit 7-2

    Removed insecure, and allowed certain writes to posts.

    ////

    ////

    ////

    Insert failed: Access denied
    Insert failed: Access denied

    ////

    • ////
    • ////
    • ////

    ////

    Securing Access To The New Post Form

    ////

    ////

    ////

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() { return Meteor.subscribe('posts'); }
    });
    
    Router.route('/', {name: 'postsList'});
    
    Router.route('/posts/:_id', {
      name: 'postPage',
      data: function() { return Posts.findOne(this.params._id); }
    });
    
    Router.route('/submit', {name: 'postSubmit'});
    
    var requireLogin = function() {
      if (! Meteor.user()) {
        this.render('accessDenied');
      } else {
        this.next();
      }
    }
    
    Router.onBeforeAction('dataNotFound', {only: 'postPage'});
    Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
    
    lib/router.js

    ////

    <template name="accessDenied">
      <div class="access-denied jumbotron">
        <h2>Access Denied</h2>
        <p>You can't get here! Please log in.</p>
      </div>
    </template>
    
    client/templates/includes/access_denied.html

    Commit 7-3

    Denied access to new posts page when not logged in.

    ////

    The access denied template
    The access denied template

    ////

    ////

    ////

    ////

    ////

    //...
    
    var requireLogin = function() {
      if (! Meteor.user()) {
        if (Meteor.loggingIn()) {
          this.render(this.loadingTemplate);
        } else {
          this.render('accessDenied');
        }
      } else {
        this.next();
      }
    }
    
    Router.onBeforeAction('dataNotFound', {only: 'postPage'});
    Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
    
    lib/router.js

    Commit 7-4

    Show a loading screen while waiting to login.

    Hiding the Link

    ////

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

    Commit 7-5

    Only show submit post link if logged in.

    ////

    Meteor Method: Better Abstraction and Security

    ////

    • ////
    • ////
    • ////

    ////

    • ////
    • ////
    • ////

    ////

    ////

    ////

    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()
        };
    
        Meteor.call('postInsert', post, function(error, result) {
          // display the error to the user and abort
          if (error)
            return alert(error.reason);
    
          Router.go('postPage', {_id: result._id});  
        });
      }
    });
    
    client/templates/posts/post_submit.js

    ////

    ////

    Security Check

    ////

    ////

    ////

    ////

    Posts = new Mongo.Collection('posts');
    
    Meteor.methods({
      postInsert: function(postAttributes) {
        check(Meteor.userId(), String);
        check(postAttributes, {
          title: String,
          url: String
        });
    
        var user = Meteor.user();
        var post = _.extend(postAttributes, {
          userId: user._id, 
          author: user.username, 
          submitted: new Date()
        });
    
        var postId = Posts.insert(post);
    
        return {
          _id: postId
        };
      }
    });
    
    lib/collections/posts.js

    ////

    Commit 7-6

    Use a method to submit the post.

    Bye Bye Allow/Deny

    ////

    ////

    Preventing Duplicates

    ////

    Meteor.methods({
      postInsert: function(postAttributes) {
        check(this.userId, String);
        check(postAttributes, {
          title: String,
          url: String
        });
    
        var postWithSameLink = Posts.findOne({url: postAttributes.url});
        if (postWithSameLink) {
          return {
            postExists: true,
            _id: postWithSameLink._id
          }
        }
    
        var user = Meteor.user();
        var post = _.extend(postAttributes, {
          userId: user._id, 
          author: user.username, 
          submitted: new Date()
        });
    
        var postId = Posts.insert(post);
    
        return {
          _id: postId
        };
      }
    });
    
    collections/posts.js

    ////

    ////

    ////

    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()
        };
    
        Meteor.call('postInsert', post, function(error, result) {
          // display the error to the user and abort
          if (error)
            return alert(error.reason);
    
          // show this result but route anyway
          if (result.postExists)
            alert('This link has already been posted');
    
          Router.go('postPage', {_id: result._id});  
        });
      }
    });
    
    client/templates/posts/post_submit.js

    Commit 7-7

    Enforce post URL uniqueness.

    Sorting Posts

    ////

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

    Commit 7-8

    Sort posts by submitted timestamp.

    ////

    ////