Now that we have moved all of our app's sensitive code into methods, we need to learn about the other half of Meteor's security story. Until now, we have worked assuming the entire database is present on the client, meaning if we call Tasks.find() we will get every task in the collection. That's not good if users of our application want to store privacy-sensitive data. We need a way of controlling which data Meteor sends to the client-side database.
Just like with insecure in the last step, all new Meteor apps start with the autopublish package, which automatically synchronizes all of the database contents to the client. Let's remove it and see what happens:
10.1 Remove autopublish package:
meteor remove autopublish
When the app refreshes, the task list will be empty. Without the autopublish package, we will have to specify explicitly what the server sends to the client. The functions in Meteor that do this are Meteor.publish and Meteor.subscribe.
First lets add a publication for all tasks:
10.2 Add publication for tasks
export const Tasks = new Mongo.Collection('tasks');if (Meteor.isServer) {// This code only runs on the serverMeteor.publish('tasks', function tasksPublication() {return Tasks.find();});}Meteor.methods({'tasks.insert'(text) {check(text, String);
And then let's subscribe to that publication when the App component is created:
10.3 Subscribe to tasks in App container
}export default withTracker(() => {Meteor.subscribe('tasks');return {tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(),incompleteCount: Tasks.find({ checked: { $ne: true } }).count(),
Once you have added this code, all of the tasks will reappear.
Calling Meteor.publish on the server registers a publication named "tasks". When Meteor.subscribe is called on the client with the publication name, the client subscribes to all the data from that publication, which in this case is all of the tasks in the database. To truly see the power of the publish/subscribe model, let's implement a feature that allows users to mark tasks as "private" so that no other users can see them.
Adding a button to make tasks private
Let's add another property to tasks called "private" and a button for users to mark a task as private. This button should only show up for the owner of a task. We want the label to indicate the current status: public or private.
First, we need to add a new method that we can call to set a task's private status:
10.4 Add tasks.setPrivate method
Tasks.update(taskId, { $set: { checked: setChecked } });},'tasks.setPrivate'(taskId, setToPrivate) {check(taskId, String);check(setToPrivate, Boolean);const task = Tasks.findOne(taskId);// Make sure only the task owner can make a task privateif (task.owner !== this.userId) {throw new Meteor.Error('not-authorized');}Tasks.update(taskId, { $set: { private: setToPrivate } });},});
Now, we need to pass a new property to the Task to decide whether we want to show the private button; the button should show up only if the currently logged in user owns this task:
10.5 Update renderTasks to pass in showPrivateButton
if (this.state.hideCompleted) {filteredTasks = filteredTasks.filter(task => !task.checked);}return filteredTasks.map((task) => {const currentUserId = this.props.currentUser && this.props.currentUser._id;const showPrivateButton = task.owner === currentUserId;return (<Taskkey={task._id}task={task}showPrivateButton={showPrivateButton}/>);});}render() {
Let's add the button, using this new prop to decide whether it should be displayed:
10.7 Add private button, shown only to owner
onClick={this.toggleChecked.bind(this)}/>{ this.props.showPrivateButton ? (<button className="toggle-private" onClick={this.togglePrivate.bind(this)}>{ this.props.task.private ? 'Private' : 'Public' }</button>) : ''}<span className="text"><strong>{this.props.task.username}</strong>: {this.props.task.text}</span>
We need to define the event handler called by the button:
10.8 Add private button event handler to Task
Meteor.call('tasks.remove', this.props.task._id);}togglePrivate() {Meteor.call('tasks.setPrivate', this.props.task._id, ! this.props.task.private);}render() {// Give tasks a different className when they are checked off,// so that we can style them nicely in CSS
One last thing, let's update the class of the <li> element in the Task component to reflect it's privacy status. We'll use the classnames NPM Package for this:
meteor npm install --save classnames
Then we'll use that package to choose a class based on the task are rendering:
10.10 Add private className to Task when needed
import React, { Component } from 'react';import { Meteor } from 'meteor/meteor';import classnames from 'classnames';import { Tasks } from '../api/tasks.js';...some lines skipped...render() {// Give tasks a different className when they are checked off,// so that we can style them nicely in CSSconst taskClassName = classnames({checked: this.props.task.checked,private: this.props.task.private,});return (<li className={taskClassName}>
Selectively publishing tasks based on privacy status
Now that we have a way of setting which tasks are private, we should modify our publication function to only send the tasks that a user is authorized to see:
10.11 Only publish tasks the current user can see
if (Meteor.isServer) {// This code only runs on the server// Only publish tasks that are public or belong to the current userMeteor.publish('tasks', function tasksPublication() {return Tasks.find({$or: [{ private: { $ne: true } },{ owner: this.userId },],});});}
To test that this functionality works, you can use your browser's private browsing mode to log in as a different user. Put the two windows side by side and mark a task private to confirm that the other user can't see it. Now make it public again and it will reappear!
Extra method security
In order to finish up our private task feature, we need to add checks to our deleteTask and setChecked methods to make sure only the task owner can delete or check off a private task:
10.12 Add extra security to methods
'tasks.remove'(taskId) {check(taskId, String);const task = Tasks.findOne(taskId);if (task.private && task.owner !== this.userId) {// If the task is private, make sure only the owner can delete itthrow new Meteor.Error('not-authorized');}Tasks.remove(taskId);},'tasks.setChecked'(taskId, setChecked) {check(taskId, String);check(setChecked, Boolean);const task = Tasks.findOne(taskId);if (task.private && task.owner !== this.userId) {// If the task is private, make sure only the owner can check it offthrow new Meteor.Error('not-authorized');}Tasks.update(taskId, { $set: { checked: setChecked } });},'tasks.setPrivate'(taskId, setToPrivate) {
Notice that with this code anyone can delete any public task. With some small modifications to the code, you should be able to make it so that only the owner can delete their tasks.
We're done with our private task feature! Now our app is secure from attackers trying to view or modify someone's private tasks.