Privacy & Security

Note: If you skipped ahead to this section, click here to download a zip of the tutorial at this point.

In this step we gonna take care of the app's security and encapsulation, since we don't want the users to do whatever they want, and we don't want them to be able to see content which is unrelevant for them.

We gonna start by removing a Meteor package named insecure.

This package provides the client with the ability to run collection mutation methods. This is a behavior we are not intrested in since removing data and creating data should be done in the server and only after certain validations.

Meteor includes this package by default only for development purposes and it should be removed once our app is ready for production.

So let's remove this package by running this command:

$ meteor remove insecure

With that we're able to add ability to remove chats:

7.2 Define 'removeChat' Method api/server/methods.ts
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
 
      Chats.insert(chat);
    },
    removeChat(chatId: string): void {
      if (!this.userId) throw new Meteor.Error('unauthorized',
        'User must be logged-in to remove chat');
 
      check(chatId, nonEmptyString);
 
      const chatExists = !!Chats.collection.find(chatId).count();
 
      if (!chatExists) throw new Meteor.Error('chat-not-exists',
        'Chat doesn\'t exist');
 
      Messages.remove({chatId});
      Chats.remove(chatId);
    },
    updateProfile(profile: Profile): void {
      if (!this.userId) throw new Meteor.Error('unauthorized',
        'User must be logged-in to create a new chat');

We have a Method, now we have to implement it in the UI.

Each chat has two buttons, one for sending attachments and one to open options menu.

Let's create that menu by creating a new component called MessagesOptionsComponent:

7.3 Create MessagesOptionsComponent src/pages/messages-options/messages-options.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import { Component } from [email protected]/core';
import { NavParams, NavController, ViewController, AlertController } from 'ionic-angular';
import { MeteorObservable } from 'meteor-rxjs';
import { TabsPage } from "../tabs/tabs";
 
@Component({
  selector: 'messages-options',
  templateUrl: 'messages-options.html'
})
export class MessagesOptionsComponent {
  constructor(
    public navCtrl: NavController,
    public viewCtrl: ViewController,
    public alertCtrl: AlertController,
    public params: NavParams
  ) {}
 
  remove(): void {
    const alert = this.alertCtrl.create({
      title: 'Remove',
      message: 'Are you sure you would like to proceed?',
      buttons: [
        {
          text: 'Cancel',
          role: 'cancel'
        },
        {
          text: 'Yes',
          handler: () => {
            this.handleRemove(alert);
            return false;
          }
        }
      ]
    });
 
    this.viewCtrl.dismiss().then(() => {
      alert.present();
    });
  }
 
  private handleRemove(alert): void {
    MeteorObservable.call('removeChat', this.params.get('chat')._id).subscribe({
      next: () => {
        alert.dismiss().then(() => {
          this.navCtrl.setRoot(TabsPage, {}, {
            animate: true
          });
        });
      },
      error: (e: Error) => {
        alert.dismiss().then(() => {
          if (e) return this.handleError(e);
 
          this.navCtrl.setRoot(TabsPage, {}, {
            animate: true
          });
        });
      }
    });
  }
 
  private handleError(e: Error): void {
    console.error(e);
 
    const alert = this.alertCtrl.create({
      title: 'Oops!',
      message: e.message,
      buttons: ['OK']
    });
 
    alert.present();
  }
}
7.4 Create MessagesOptionsComponent view src/pages/messages-options/messages-options.html
1
2
3
4
5
6
7
8
<ion-content class="chats-options-page-content">
  <ion-list class="options">
    <button ion-item class="option option-remove" (click)="remove()">
      <ion-icon name="remove" class="option-icon"></ion-icon>
      <div class="option-name">Remove</div>
    </button>
  </ion-list>
</ion-content>
7.5 Create MessagesOptionsComponent view style src/pages/messages-options/messages-options.scss
1
2
3
4
5
6
7
8
9
10
11
12
13
.chats-options-page-content {
  .options {
    margin: 0;
  }
 
  .option-name {
    float: left;
  }
 
  .option-icon {
    float: right;
  }
}
7.6 Register the component src/app/app.module.ts
10
11
12
13
14
15
16
 
23
24
25
26
27
28
29
 
39
40
41
42
43
44
45
46
import { ProfileComponent } from "../pages/profile/profile";
import { ChatsOptionsComponent } from "../pages/chat-options/chat-options";
import { NewChatComponent } from "../pages/new-chat/new-chat";
import { MessagesOptionsComponent } from "../pages/messages-options/messages-options";
 
@NgModule({
  declarations: [
...some lines skipped...
    ProfileComponent,
    ChatsOptionsComponent,
    NewChatComponent,
    MessagesOptionsComponent
  ],
  imports: [
    IonicModule.forRoot(MyApp),
...some lines skipped...
    VerificationComponent,
    ProfileComponent,
    ChatsOptionsComponent,
    NewChatComponent,
    MessagesOptionsComponent
  ],
  providers: []
})

Great! Now we can define component's method to open the options:

7.7 Define component's method to open options src/pages/messages/messages.ts
1
2
3
4
5
6
7
8
9
10
11
12
 
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { Component, OnInit, OnDestroy, ElementRef } from "@angular/core";
import { NavParams, PopoverController } from "ionic-angular";
import { Chat, Message } from "api/models/whatsapp-models";
import { Messages } from "api/collections/whatsapp-collections";
import { Observable, Subscription } from "rxjs";
import { MeteorObservable } from "meteor-rxjs";
import { MessagesOptionsComponent } from "../messages-options/messages-options";
 
declare let Meteor;
 
@Component({
  selector: "messages-page",
...some lines skipped...
  autoScroller: Subscription;
  senderId: string;
 
  constructor(navParams: NavParams, element: ElementRef, public popoverCtrl: PopoverController) {
    this.selectedChat = <Chat>navParams.get('chat');
    this.title = this.selectedChat.title;
    this.picture = this.selectedChat.picture;
    this.senderId = Meteor.userId();
  }
 
  showOptions(): void {
    const popover = this.popoverCtrl.create(MessagesOptionsComponent, {
      chat: this.selectedChat
    }, {
      cssClass: 'options-popover'
    });
 
    popover.present();
  }
 
  private get messagesPageContent(): Element {

One thing missing, add this method to the view:

7.8 Implement it in the view src/pages/messages/messages.html
8
9
10
11
12
13
14
 
    <ion-buttons end>
      <button ion-button icon-only class="attach-button"><ion-icon name="attach"></ion-icon></button>
      <button ion-button icon-only class="settings-button" (click)="showOptions()"><ion-icon name="more"></ion-icon></button>
    </ion-buttons>
  </ion-navbar>
</ion-header>

And let's use the chat removal method in the chats view once we slide a chat item to the right and press the remove button:

7.9 Use chat removal method in cahts component src/pages/chats/chats.ts
2
3
4
5
6
7
8
 
19
20
21
22
23
24
25
26
 
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import { Observable } from "rxjs";
import { Chat } from "api/models/whatsapp-models";
import { Chats, Messages, Users } from "api/collections/whatsapp-collections";
import { NavController, PopoverController, ModalController, AlertController } from "ionic-angular";
import { MessagesPage } from "../messages/messages";
import { ChatsOptionsComponent } from "../chat-options/chat-options";
import { NewChatComponent } from "../new-chat/new-chat";
...some lines skipped...
  constructor(
    public navCtrl: NavController,
    public popoverCtrl: PopoverController,
    public modalCtrl: ModalController,
    public alertCtrl: AlertController
  ) {}
 
  ngOnInit() {
...some lines skipped...
  }
 
  removeChat(chat: Chat): void {
    MeteorObservable.call('removeChat', chat._id).subscribe({
      error: (e: Error) => {
        if (e) this.handleError(e);
      }
    });
  }
 
  private handleError(e: Error): void {
    console.error(e);
 
    const alert = this.alertCtrl.create({
      title: 'Oops!',
      message: e.message,
      buttons: ['OK']
    });
 
    alert.present();
  }
}

Right now all the chats are published to all the clients which is not very good for privacy. Let's fix that.

First thing we need to do in order to stop all the automatic publication of information is to remove the autopublish package from the Meteor server:

$ meteor remove autopublish

Now we need to explicitly define our publications. Let's start by sending the users' information:

7.11 Added users publication api/server/publications.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { Users, Messages, Chats } from '../collections/whatsapp-collections';
import { User, Message, Chat } from 'api/models/whatsapp-models';
 
export function initPublications() {
  Meteor.publish('users', function(): Mongo.Cursor<User> {
    if (!this.userId) return;
 
    return Users.collection.find({}, {
      fields: {
        profile: 1
      }
    });
  });
 
}

And add the messages:

7.12 Added messages publication api/server/publications.ts
14
15
16
17
18
19
20
21
22
23
24
    });
  });
 
  Meteor.publish('messages', function(chatId: string): Mongo.Cursor<Message> {
    if (!this.userId) return;
    if (!chatId) return;
 
    return Messages.collection.find({chatId});
  });
 
}

We will now add the publish-composite package which will help us implement joined collection pubications.

$ meteor add reywood:publish-composite

Use Meteor.publishComposite from the package we installed and create a publication of Chats:

7.14 Added chats publication api/server/publications.ts
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
    return Messages.collection.find({chatId});
  });
 
  Meteor.publishComposite('chats', function() {
    if (!this.userId) return;
 
    return {
      find: () => {
        return Chats.collection.find({memberIds: this.userId});
      },
 
      children: [
        {
          find: (chat) => {
            return Messages.collection.find({chatId: chat._id}, {
              sort: {createdAt: -1},
              limit: 1
            });
          }
        },
        {
          find: (chat) => {
            return Users.collection.find({
              _id: {$in: chat.memberIds}
            }, {
              fields: {profile: 1}
            });
          }
        }
      ]
    };
  });
}

The chats publication is a composite publication which is made of several nodes. First we gonna find all the relevant chats for the current user logged in. After we have the chats, we gonna return the following cursor for each chat document we found. First we gonna return all the last messages, and second we gonna return all the users we're currently chatting with.

Those publications are still not visible by server, we need to import and run the init method:

7.15 Init publication on server start api/server/main.ts
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Users } from "../collections/whatsapp-collections";
import { initMethods } from "./methods";
import { Accounts } from 'meteor/accounts-base';
import { initPublications } from "./publications";
 
declare let SMS, Object;
 
Meteor.startup(() => {
  initMethods();
  initPublications();
 
  if (Meteor.settings) {
    Object.assign(Accounts._options, Meteor.settings['accounts-phone']);

Let's add the subscription for the chats publication in the chats component:

7.16 Subscribe to 'chats' src/pages/chats/chats.ts
6
7
8
9
10
11
12
 
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import { MessagesPage } from "../messages/messages";
import { ChatsOptionsComponent } from "../chat-options/chat-options";
import { NewChatComponent } from "../new-chat/new-chat";
import { MeteorObservable } from 'meteor-rxjs';
 
declare let Meteor;
 
...some lines skipped...
  ngOnInit() {
    this.senderId = Meteor.userId();
 
    MeteorObservable.subscribe('chats').subscribe(() => {
      MeteorObservable.autorun().subscribe(() => {
        if (this.chats) {
          this.chats.unsubscribe();
          this.chats = undefined;
        }
 
        this.chats = Chats
          .find({})
          .mergeMap((chats: Chat[]) =>
            Observable.combineLatest(
              ...chats.map((chat: Chat) =>
                Messages
                  .find({chatId: chat._id})
                  .startWith(null)
                  .map(messages => {
                    if (messages) chat.lastMessage = messages[0];
                    return chat;
                  })
              )
            )
          ).map(chats => {
            chats.forEach(chat => {
              chat.title = '';
              chat.picture = '';
 
              const receiver = Users.findOne(chat.memberIds.find(memberId => memberId !== this.senderId));
              if (!receiver) return;
 
              chat.title = receiver.profile.name;
              chat.picture = receiver.profile.picture;
            });
 
            return chats;
          }).zone();
      });
    });
  }
 
  addChat(): void {

The users publication publishes all the users' profiles, and we need to use it in the new chat dialog whenever we wanna create a new chat.

Let's subscribe to the users publication in the new chat component:

7.17 Subscribe to 'users' src/pages/new-chat/new-chat.ts
24
25
26
27
28
29
30
31
32
33
  }
 
  ngOnInit() {
    MeteorObservable.subscribe('users').subscribe(() => {
      MeteorObservable.autorun().subscribe(() => {
        this.users = this.findUsers().zone();
      });
    });
  }
 

The messages publication is responsible for bringing all the relevant messages for a certain chat. This publication is actually parameterized and it requires us to pass a chat id during subscription.

Let's subscribe to the messages publication in the messages component, and pass the current active chat id provided to us by the nav params:

7.18 Subscribe to 'messages' src/pages/messages/messages.ts
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
  }
 
  ngOnInit() {
    MeteorObservable.subscribe('messages', this.selectedChat._id).subscribe(() => {
      MeteorObservable.autorun().subscribe(() => {
        this.messages = Messages.find(
          {chatId: this.selectedChat._id},
          {sort: {createdAt: 1}}
        ).map((messages: Message[]) => {
          messages.forEach((message: Message) => {
            message.ownership = this.senderId == message.senderId ? 'mine' : 'other';
          });
 
          return messages;
        });
      });
    });
 
    this.autoScroller = MeteorObservable.autorun().subscribe(() => {