Tạo Bài Viết

7

Percentage Translated

In this chapter, you will:

  • Học cách làm thế nào để submit bài viết phía client
  • Thực hiện kiểm tra bảo mật đơn giản
  • Hạn chế truy cập tới form submit bài viết.
  • Học cách dùng method phía server để bảo mật hơn.
  • Chúng ta đã thấy được việc tạo bài viết thông qua console dễ dàng như thế nào bằng việc gọi tương tác cơ sở dữ liệu Posts.insert. Tuy nhiên chúng ta không mong người dùng mở console và tạo bài viết như vậy.

    Dần dần, chúng ta cần xây dựng giao diện người dùng để gửi những bài viết mới cho ứng dụng.

    Xây dựng trang tạo bài viết mới

    Chúng ta bắt đầu bằng việc tạo route cho trang mới:

    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

    Thêm Đường Dẫn Cho Header

    Với route đã được định nghĩa, chúng ta có thể thêm đường dẫn tới trang submit vào phần 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

    Thiết lập route nghĩa là nếu người dùng truy cập vào URL /submit, Meteor sẽ hiển thị template postSubmit. Vì vậy hãy bắt đầu viết template:

    <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

    Chú ý: có rất nhiều markup ở đây, tuy nhiên nó đơn giản tới từ Twitter Bootstrap. Trong khi chỉ có thành phần form là cần thiết, tất cả markup khác giúp ứng dụng của chúng ta trông đẹp mắt hơn. Hiển thị trông sẽ giống như sau:

    The post submit form
    The post submit form

    ////

    Tạo bài viết

    Hãy gắn kết trình xử lý sự kiện (event handler) tới sự kiện submit form. Tốt nhất là chúng ta sử dụng sự kiện submit (hơn là nói rằng sự kiện click trên một button), vì nó sẽ bao hàm tất cả các cách để submit (ví dụ như bấm enter).

    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.

    Hàm này sử dụng jQuery để phân tách giá trị (value) của nhiều trường trong form của chúng ta, và tạo ra một object post mới từ kết quả phân tách. Chúng ta cần chắc chắn là chúng ta preventDefault trên tham số event của handler để chắc chắn là trình duyệt không tự ý submit form.

    Cuối cùng, chúng ta có thể định tuyến đến trang bài viết mới. Hàm insert() đối với một collection trả lại _id của object đã được chèn vào cơ sở dữ liệu, chính là cái mà hàm Router go() sẽ dùng để tạo ra URL cho chúng ta truy cập.

    Kết quả đạt được là khi người dùng bấm submit, một bài viết mới được tạo ra, và người dùng sẽ ngay lập tức được đưa tới trang thảo luận cho bài viết mới đó.

    Thêm một vài thiết lập an ninh

    Tạo bài viết là một việc rất tốt, nhưng chúng ta không muốn để ất cả người thăm quan ngẫu nhiên làm điều đó: chúng ta muốn họ phải log in trước. Dĩ nhiên, chúng ta cũng có thể che giấu form tạo bài viết đối với người dùng đã log out. Tuy nhiên, người dùng vẫn có thể tạo bài viết trên console trình duyệt mà không cần log in, và chúng ta không muốn điều đó.

    Rất biết ơn là an toàn dữ liệu được để sẵn bên trong collection Meteor; chỉ có điều là nó được tắt mặc định khi tạo project. Điều này giúp cho bạn bắt đầu một cách dễ dàng và bắt đầu xây dựng ứng dụng của bạn trong khi tạm để dành những phần nhàm chán đó về sau.

    Ứng dụng của chúng ta không cần những bánh xe thực tập đó nữa, vì vậy hãy tháo bỏ chúng! Chúng ta sẽ xoá gói insecure:

    meteor remove insecure
    
    Terminal

    Sau khi làm điều đó, bạn sẽ nhận ra là form bài viết không còn làm việc một cách hiệu quả. Bởi vì không có gói insecure, việc chèn bài viết vào collection posts từ phía client không được chấp nhận nữa.

    Chúng ta phải hoặc là thiết lập một số luật báo cho Meteor biết khi nào client được phép chèn bài viết, hoặc là làm việc đó ở phía server.

    Cho phép chèn bài viết

    Để bắt đầu, chúng ta sẽ chỉ ra làm thế nào để cho phép client chèn bài viết, giúp cho form trở lại hoạt động bình thường. Chúng ta sẽ sử dụng một kỹ thuật khác, nhưng bây giờ, thứ sau đây sẽ giúp cho mọi thứ trở lại hoạt động:

    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.

    Chúng ta gọi Posts.allow, báo cho Meteor biết là “đây là một hoàn cảnh mà client được phép thực hiện thao tác tới collection Posts”. Trong trường hợp này, chúng ta bảo rằng “client được phép chèn bài viết miễn là nó có userId”.

    userId của user thực hiện việc thay đổi được chuyển qua gọi allowdeny (hoặc trả về null nếu không có user nào log in), là thứ mà hầu như lúc nào cũng có ích. Và do tài khoản người dùng được gắn với phần lõi của Meteor, chúng ta có thể tin tưởng rằng userId luôn đúng.

    Chúng ta vừa thành công trong việc chắc chắn rằng bạn cần phải log in để tạo bài viết. Thử log out và tạo một bài viết; bạn sẽ thấy như sau trên console:

    Insert failed: Access denied
    Insert failed: Access denied

    Tuy nhiên, chúng ta vẫn phải đổi mặt với một vài vấn đề:

    • Người dùng trong trạng thái log out vẫn có thể tiếp cận tạo form bài viết.
    • Bài viết không được gắn với user theo bất kỳ cách nào (và không có đoạn code nào trên server bắt buộc điều đó).
    • Nhiều bài viết có thể được tạo với cùng URL

    Hãy cùng nhau sửa những vấn đề này.

    Thiết lập an ninh cho truy cập form tạo bài viết mới

    Chúng ta sẽ bắt đầu bằng việc ngăn người dùng log out từ việc thấy form submit bài viết. Chúng ta sẽ làm điều đó từ cấp router, bằng việc định nghĩa route hook.

    Hook ngăn chặn xử lý định tuyến và thay đổi hành động mà router mặc định đảm đương. Bạn có thể nghĩ nó như là một người bảo vệ kiểm tra giấy chứng nhận trước khi để bạn vào (hoặc thoát ra).

    Điều chúng ta cần làm là kiểm tra người dùng đã log in hay chưa, và nếu chưa thì đưa ra template accessDenied thay vì template postSubmit (chúng ta sẽ dừng router không cho làm bất kỳ điều gì khác). Vậy hãy thay đổi router.js như sau:

    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

    Chúng ta cũng tạo template cho trang từ chối truy cập:

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

    Nếu bạn truy cập vào http://localhost:3000/submit/ mà không log in, bạn sẽ nhận được tin nhắn như bên dưới:

    The access denied template
    The access denied template

    Điều hay về route hook là nó cũng tương tác ngược. Điều đó có nghĩa là chúng ta không cần nghĩ về việc thiết lập callback mỗi khi người dùng log in: khi tr thái log in của người dùng thay đổi, template của trang Router ngay lập tức chuyển từ accessDenied thành postSubmit mà chúng ta không cần phải viết bất kỳ đoạn code nào để xử lý việc đó (và tiện thể, điều này hoạt động ngay cả với nhiều tab trình duyệt).

    Log in và thử làm mới trang. Bạn sẽ có thể nhận ra là template trang từ chối truy cập thỉnh thoảng loé hiện ra trong một khoảnh khắc trước khi trang submit xuất hiện. Lý do cho điều này là Meteor bắt đầu đưa ra template ngay khi có thể, trước khi nó kịp nói chuyện với server và kiểm tra nếu người dùng hiện tại (được lưu trữ trên bộ lưu trữ cục bộ trên trình duyệt) tồn tại hay không.

    Để tránh điều này (là một vấn đề chung bạn sẽ gặp nhiều hơn nếu xử lý với những rắc rối xuất phát từ độ trễ giữa client và server). Chúng ta sẽ chỉ hiển thị một màn hình đang nạp cho khoảnh khắc ngắn mà chúng ta đợi để biết nếu như người dùng đã truy cập hay chưa.

    Sau hết thì tại thời điển này, chúng ta không biết nếu như người dùng đúng là có chứng thực log-in đúng hay không, và chúng ta không thể hiển thị cả template accessDenied hoặc postSubmit cho đến khi chúng ta xác nhận được.

    Bởi vậy chúng ta sẽ thay đổi hook để sử dụng template đang nạp khi mà Meteor.loggingIn() ở trạng thái đúng:

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

    Giấu đường dẫn

    Cách dễ dàng nhất để tránh người dùng do nhầm lẫn mà cố gắng truy cập tới trang này khi mà họ đã log out là giấu đường dẫn đó đi. Chúng ta có thể làm điều này khá dễ dàng:

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

    helper currentUser được cung cấp cho chúng ta bằng gói accounts và là Spacebars tương ứng với Meteor.user(). Vì nó tương tác ngược, đường dẫn sẽ xuất hiện hoặc biến mất khi bạn log in và log out.

    Meteor Method: Trừ tượng hoá tốt hơn và an toàn hơn

    Chúng ta vừa thành công trong việc bảo mật lối vào tới bài viết mới từ người dùng log out, và từ chối những người dùng đó từ việc tạo bài viết ngay cả khi họ gian lận và dùng console. Tuy nhiên vẫn còn một vài thứ chúng ta phải để ý:

    • Đánh dấu thời gian của bài viết.
    • Chắc chắn rằng URL giống nhau không được tạo nhiều hơn một lần.
    • Thêm thông tin chi tiết về tác giả bài viết (ID, username,…).

    Bạn có thể nghĩ rằng chúng ta có thể làm điều đó thông qua handler sự kiện submit. Thực tế là chúng ta sẽ nhanh chóng gặp phải những vấn đề sau.

    • Về nhãn thời gian, chúng ta phải dựa vào thời gian phía máy của người dùng, điều đó không luôn đúng trong mọi trường hợp.
    • Client sẽ không biết được tất cả URL được gửi tới trang. Chúng chỉ biết bài viết đang được hiển thị (chúng ta sẽ thấy điều này sau đây), vì vậy không có cách nào để cho URL đơn nhất phía client.
    • Cuối cùng, mặc dù chúng ta có thể thêm cụ thể người dùng từ phía client, chúng ta sẽ không bắt buộc được độ chính xác của nó, điều có thể dẫn đến ứng dụng của chúng ta bị khai thác hết từ người dùng console trình duyệt.

    Cho những lý do đó, tốt hơn là giữ cho handler sự kiện đơn giản, và nếu chúng ta làm nhiều hơn là thao tác chèn hoặc sửa đơn giản tới collection, chúng ta sử dụng Method.

    Một Meteor Method là một hàm phía server được gọi (call) bởi phía client. Chúng ta không phải là hoàn toàn không biết về nó – thực tế, phía sau màn hình, thao tác insert, updateremove của Collection đều là Method. Hãy xem làm thế nào để tạo ra Method của riêng chúng ta.

    ãy cùng trở lại với post_submit.js. Thay vì chèn trực tiếp vào collection Posts, chúng ta sẽ gọi Method tên là postInsert:

    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

    Hàm Meteor.call gọi tên một Method ở tham số thứ nhất. Bạn có thể cung cấp tham số cho hàm call (trong trường hợp này, là object post chúng ta cấu tạo từ form), và cuối cùng gắn vào hàm callback, thứ sẽ chạy khi mà Method ở phía server đã thực hiện xong.

    Callback của Meteor method luôn có hai tham số, errorresult. Nếu vì bất kỳ lý do gì mà error tồn tại, chúng ta sẽ thông báo alert cho người dùng (sử dụng return để huỷ bỏ callback). Nếu như mọi thứ hoạt đúng, chúng ta sẽ đổi hướng thành công người dùng sang trang thảo luận cho bài viết vừa được tạo.

    Kiểm tra bảo mật

    Chúng ta sẽ sử dụng cơ hội này để thêm vào một số thuộc tính bảo mật cho method bằng việc sử dụng gói audit-argument-checks.

    Gói này giúp chúng ta kiểm tra object JavaScript theo một chuẩn định nghĩa trước. Trong trường hợp này, chúng ta sẽ dùng nó để kiểm tra xem người dùng gọi method đã log in hay chưa (bằng việc chắc chắn Meteor.userId() là một chuỗi String), và xem object postAttributes được gửi như là một tham số tới method bao gồm chuỗi titleurl hay không. Theo cách này, chúng ta không để cho những mảnh dữ liệu ngẫu nhiên vào cơ sở dữ liệu.

    Hãy cùng định nghĩa method postInsert trong file collections/posts.js. Chúng ta sẽ xoá khối allow() từ posts.js bởi vì Meteor Methods dù sao bỏ qua nó.

    Chúng ta cũng sẽ sau đó mở rộng (extend) object postAttributes với thêm ba thuộc tính: _idusername của người dùng, cũng như tem thời gian lúc gửi bài submitted trước khi chèn toàn bộ mọi thứ vào cơ sở dữ liệu và trả về kết quả _id tới client (nói cách khác, gọi ban đầu của method) bởi một object JavaScript.

    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.

    Tạm biệt Allow/Deny

    Chú ý rằng method _.extend() là một phần của thư viện Underscore, nó đơn giản giúp bạn “mở rộng” một object với những thuộc tính khác.

    Nếu bạn muốn chạy một đoạn code trước mỗi lệnh insert, update hoặc remove ngay cả trên server, chúng tôi đề nghị kiểm tra gói collection-hooks.

    Tránh bản sao (duplicate)

    Chúng ta sẽ tạo thêm kiểm tra nữa trước khi đóng gọi method. Nếu như một bài viết cùng URL đã được tạo trước đó, chúng ta sẽ không thêm đường dẫn lần thứ hai mà sẽ hướng người dùng sang bài viết đã tồn tại.

    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

    Chúng ta đang tìm kiếm trên cơ sở dữ liệu của mình bất kỳ bài viết nào có cùng URL. Nếu như bất kỳ cái nào tìm được, chúng ta return _id của bài viết đó kèm với cờ postExists: true để cho client biết về tình huống này.

    Và do chúng ta đã khởi động gọi return, method dừng lại tại thời điểm đó mà không chạy câu lệnh insert, do đó tránh được bản sao.

    Điều còn lại là dùng thông tin postExists này để tạo mẩu tin cảnh báo tới helper sự kiện phía client:

    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.

    Sắp xếp bài viết

    Bây giờ chúng ta đã có ngày submit trên tất cả bài viết, và sẽ rất hợp lý nếu chúng được sắp xếp theo thuộc tính này. Để làm điều đó, chúng ta có thể sử dụng toán tử sort của Meteor. Toán tử này bao gồm object có chứa khoá sắp xếp và một dấu chỉ ra chúng được sắp xếp tăng hay giảm.

    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.

    Chúng ta đã làm việc nhiều một chút, nhưng cuối cùng có được một giao diện để người dùng nhập nội vào ứng dụng một cách an toàn!

    Nhưng một ứng dụng cho phép tạo nội dung cũng đồng thời phải có cách để biên tập hoặc xoá nội dung. Đó sẽ là điều được bàn đến trong chương tiếp theo.