In this section we'll look at how to:
Right now, our app is publishing all parties to all clients, allowing any client to change those parties. The changes are then reflected back to all the other clients automatically.
This is super powerful and easy, but what about security? We don't want any user to be able to change any party...
For quick and easy setup, Meteor automatically includes a package called insecure
. As the name implies, the packages provides a default behavior to Meteor collections allowing all reads and writes.
The first thing we should do is to remove the "insecure" package. By removing that package, the default behavior is changed to deny all.
Execute this command in the command line:
$ meteor remove insecure
> insecure removed from your project
Let's try to change the parties array or a specific party. Nothing's working.
That's because now we have to write an explicit security rule for each operation we want to make on the Mongo collection.
We can assume we will allow a user to alter data if any of the following are true:
One of Meteor's most powerful packages is the Meteor accounts system.
Add the "accounts-password" Meteor package. It's a very powerful package for all the user operations you can think of: login, signup, change password, password recovery, email confirmation and more.
$ meteor add accounts-password
Now we are going to add angular2-meteor-accounts-ui
which is a package that contains all the HTML and CSS we need for the user operation forms.
$ meteor npm install --save angular2-meteor-accounts-ui
Because Angular 2 works with modules, we need to import this package's module into our:
2
3
4
5
6
7
8
13
14
15
16
17
18
19
20
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { AccountsModule } from 'angular2-meteor-accounts-ui';
import { AppComponent } from './app.component';
import { routes } from './app.routes';
...some lines skipped...
BrowserModule,
FormsModule,
ReactiveFormsModule,
RouterModule.forRoot(routes),
AccountsModule
],
declarations: [
AppComponent,
Let's add the <login-buttons>
tag below of the party form in the PartiesList's template:
1
2
3
4
5
6
<div>
<parties-form></parties-form>
<login-buttons></login-buttons>
<ul>
<li *ngFor="let party of parties | async">
Run the code, you'll see a login link below the form. Click on the link and then "create account" to sign up. Try to log in and log out.
That's it! As you can see, it's very easy to add basic login support with the help of the Meteor accounts package.
Now that we have our account system, we can start defining our security rules for the parties.
Let's go to the "collection" folder and specify what actions are allowed:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { MongoObservable } from 'meteor-rxjs';
import { Meteor } from 'meteor/meteor';
import { Party } from '../models/party.model';
export const Parties = new MongoObservable.Collection<Party>('parties');
function loggedIn() {
return !!Meteor.user();
}
Parties.allow({
insert: loggedIn,
update: loggedIn,
remove: loggedIn
});
In only 10 lines of code we've specified that inserts, updates and removes can only be completed if a user is logged in.
The callbacks passed to the Parties.allow are executed on the server only. The client optimistically assumes that any action (such as removal of a party) will succeed, and reverts the action as soon as the server denies permission. If you want to learn more about those parameters passed into Parties.allow or how this method works in general, please, read the official Meteor docs on allow.
Let's work on ensuring only the party creator (owner) can change the party data.
First we must define an owner for each party that gets created. We do this by taking our current user's ID and setting it as the owner ID of the created party.
Meteor's base accounts package provides two reactive functions that we are going to
use, Meteor.user()
and Meteor.userId()
.
For now, we are going to keep it simple in this app and allow every logged-in user to change a party. It'd be useful to add an alert prompting the user to log in if she wants to add or update a party.
Change the click handler of the "Add" button in the parties-form.component.ts
, addParty
:
1
2
3
4
5
6
26
27
28
29
30
31
32
33
34
35
36
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Meteor } from 'meteor/meteor';
import { Parties } from '../../../../both/collections/parties.collection';
...some lines skipped...
}
addParty(): void {
if (!Meteor.userId()) {
alert('Please log in to add a party');
return;
}
if (this.addForm.valid) {
Parties.insert(this.addForm.value);
Now, change it to save the user ID as well:
32
33
34
35
36
37
38
}
if (this.addForm.valid) {
Parties.insert(Object.assign({}, this.addForm.value, { owner: Meteor.userId() }));
this.addForm.reset();
}
Notice that you'll need to update the Party interface in the party.interface.ts
definition file with the optional new property: owner?: string
:
4
5
6
7
8
name: string;
description: string;
location: string;
owner?: string;
}
Let's verify the same logic for updating a party:
1
2
3
4
5
6
7
34
35
36
37
38
39
40
41
42
43
44
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';
import { Meteor } from 'meteor/meteor';
import 'rxjs/add/operator/map';
...some lines skipped...
}
saveParty() {
if (!Meteor.userId()) {
alert('Please log in to change this party');
return;
}
Parties.update(this.party._id, {
$set: {
name: this.party.name,
CanActivate
is a one of three guard types in the new router. It decides if a route can be activated.
Now you can specify if a component can be accessed only when a user is logged in using the canActivate
property in the router definition.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Route } from '@angular/router';
import { Meteor } from 'meteor/meteor';
import { PartiesListComponent } from './parties/parties-list.component';
import { PartyDetailsComponent } from './parties/party-details.component';
export const routes: Route[] = [
{ path: '', component: PartiesListComponent },
{ path: 'party/:partyId', component: PartyDetailsComponent, canActivate: ['canActivateForLoggedIn'] }
];
export const ROUTES_PROVIDERS = [{
provide: 'canActivateForLoggedIn',
useValue: () => !! Meteor.userId()
}];
We created a new provider called canActivateForLoggedIn
that contains a boolean value with login state.
As you can see we specified only the name of that provider inside canActivate
property.
It's worth mentioning that guards can receive more than one provider.
Now, we only need to declare this provider in our NgModule:
5
6
7
8
9
10
11
20
21
22
23
24
25
26
27
28
import { AccountsModule } from 'angular2-meteor-accounts-ui';
import { AppComponent } from './app.component';
import { routes, ROUTES_PROVIDERS } from './app.routes';
import { PARTIES_DECLARATIONS } from './parties';
@NgModule({
...some lines skipped...
AppComponent,
...PARTIES_DECLARATIONS
],
providers: [
...ROUTES_PROVIDERS
],
bootstrap: [
AppComponent
]
If you place @InjectUser
above the PartiesFormComponent
it will inject a new user property:
client/imports/app/parties/parties-form.component.ts
:
import { InjectUser } from 'angular2-meteor-accounts-ui';
import { Meteor } from 'meteor/meteor';
import { OnInit } from '@angular/core';
import template from './parties-form.component.html';
@Component({
selector: 'parties-form',
template,
})
@InjectUser('user')
export class PartiesFormComponent implements OnInit {
user: Meteor.User;
ngOnInit() {
console.log(this.user);
}
}
Call this.user
and you will see that it returns the same object as Meteor.user()
.
The new property is reactive and can be used in any template, for example:
client/imports/app/parties/parties-form.component.html
:
<div *ngIf="!user">Please, log in to change party</div>
<form ...>
...
</form>
As you can see, we've added a label "Please, login to change party" that is
conditioned to be shown if user
is not defined with help of an ngIf
attribute, and
will be hidden otherwise.
Let's imagine now that we allow to see and change party details only for logged-in users. An ideal way to implement this would be to restrict redirecting to the party details page when someone clicks on a party link. In this case, we don't need to check access manually in the party details component itself because the route request is denied early on.
This can be easily done again with help of CanActivate
property. You can do this with the PartyDetailsComponent, just like we did previous steps earlier with the PartiesFormComponent.
Now log out and try to click on any party link. See, links don't work!
But what about more sophisticated access? Say, let's prevent access into the PartyDetails view for those who don't own that particular party.
It could be done inside of a component using canActivate
method.
Let's add a canActivate
method and CanActivate
interface, where we get the current route's partyId
parameter
and check if the corresponding party's owner is the same as the currently logged-in user.
client/imports/app/parties/party-details.component.ts
:
import { CanActivate } from '@angular/router';
import template from './party-details.component.html';
@Component({
selector: 'party-details',
template
})
export class PartyDetails implements CanActivate {
// ...
canActivate() {
const party = Parties.findOne(this.partyId);
return (party && party.owner == Meteor.userId());
}
}
Now log in, then add a new party, log out and click on the party link. Nothing happens meaning that access is restricted to party owners.
Please note it is possible for someone with malicious intent to override your routing restrictions on the client. You should never restrict access to sensitive data, sensitive areas, using the client router only.
This is the reason we also made restrictions on the server using the allow/deny functionality, so even if someone gets in they cannot make updates. While this prevents writes from happening from unintended sources, reads can still be an issue. The next step will take care of privacy, not showing users parties they are not allowed to see.
Amazing, only a few lines of code and we have a much more secure application!
We've added two powerful features to our app: