-
Notifications
You must be signed in to change notification settings - Fork 32
/
07-creating-posts.md.erb
485 lines (356 loc) · 22 KB
/
07-creating-posts.md.erb
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
---
title: Créer des Posts
slug: creating-posts
date: 0007/01/01
number: 7
points: 5
photoUrl: http://www.flickr.com/photos/markezell/9688179085
photoAuthor: Mark Ezell
contents: Apprendre à envoyer un post côté client.|Implémenter une vérification de sécurité simple.|Restreindre l'accès au formulaire d'envoi de post.|Apprendre à utiliser une Method côté serveur pour plus de sécurité.
paragraphs: 60
---
Nous avons vu combien il était facile de créer des posts en passant par la console, avec l'appel à la base de données `Posts.insert`. Mais nous ne pouvons pas attendre de nos utilisateurs qu'ils ouvrent la console pour créer un nouveau post.
À un moment, nous devrons créer une interface pour permettre à nos utilisateurs de poster de nouvelles histoires sur notre application.
### Construire la page d'ajout de post
Nous commençons par définir une route pour notre nouvelle page :
~~~js
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'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "15" %>
### Ajouter un lien à l'en-tête
Une fois cette route définie, nous pouvons maintenant ajouter un lien à notre page d'envoi dans notre en-tête :
~~~html
<template name="header">
<nav class="navbar navbar-default" role="navigation">
<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'}}">Créer un post</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{{> loginButtons}}
</ul>
</div>
</nav>
</template>
~~~
<%= caption "client/templates/includes/header.html" %>
<%= highlight "14~16" %>
Configurer notre route signifie que si un utilisateur se rend à l'URL `/submit`, Meteor affichera le template `postSubmit`. Écrivons donc ce template :
~~~html
<template name="postSubmit">
<form class="main form page">
<div class="form-group">
<label class="control-label" for="url">URL</label>
<div class="controls">
<input name="url" id="url" type="text" value="" placeholder="Votre 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="Nommez votre article" class="form-control"/>
</div>
</div>
<input type="submit" value="Submit" class="btn btn-primary"/>
</form>
</template>
~~~
<%= caption "client/templates/posts/post_submit.html" %>
Note : ça fait beaucoup de code, mais ça vient simplement du fait que nous utilisons Twitter Bootstrap. Bien que seuls les éléments de formulaire soient essentiels, les balises supplémentaires aideront à rendre notre appli un peu plus jolie. Cela devrait maintenant ressembler à ça :
<%= screenshot "7-1", "Le formulaire de soumission d'article" %>
C'est un formulaire simple. Nous n'avons pas à nous inquiéter de lui ajouter un champ 'action', puisque nous allons intercepter les événements sur le formulaire et mettre à jour les données en passant par JavaScript. Il n'y aurait pas de sens à fournir une solution alternative sans JavaScript quand on considère qu'une appli Meteor ne fonctionne plus du tout si JavaScript est désactivé.
### Créer des posts
Lions un gestionnaire d'événement à l'événement `submit` du formulaire. Il est préférable d'utiliser l'événement `submit` (plutôt qu'un événement `click` sur le bouton par exemple), car cela permettra de prendre en compte toutes les façons possibles d'envoyer le formulaire (comme appuyer sur entrée par exemple).
~~~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()
};
post._id = Posts.insert(post);
Router.go('postPage', post);
}
});
~~~
<%= caption "client/templates/posts/post_submit.js" %>
<%= commit "7-1", "Avec une page d'ajout de post et un lien vers celle-ci dans l'en-tête." %>
Cette fonction utilise [jQuery](http://jquery.com) pour vérifier les valeurs de nos divers champs de formulaire et construire un nouvel objet post à partir des résultats. Nous devons nous assurer d'utiliser `preventDefault` sur l'argument `event` de notre gestionnaire pour être sûr que le navigateur n'essaie pas de prendre les devants en essayant d'envoyer le formulaire.
Finalement, nous pouvons router l'utilisateur vers la page de son nouveau post. La fonction `insert()` (utilisée sur une collection), renvoie l'`id` généré pour l'objet qui vient d'être inséré dans la base de données. La fonction `go()` du Router va utiliser cet `id` pour construire l'URL correspondante, et nous amener sur la bonne page.
Ainsi, l'utilisateur envoie le formulaire, un nouveau post est créé, et l'utilisateur est instantanément dirigé vers la page de discussion de ce nouveau post.
### Ajouter un peu de sécurité
Créer des posts est très bien, mais nous ne voulons pas laisser n'importe quel visiteur le faire : ils doivent être authentifiés pour pouvoir poster. Bien sûr, nous pouvons commencer en cachant la page d'ajout de post pour les utilisateurs non authentifiés. Cependant un utilisateur pourrait créer un post depuis la console du navigateur sans être authentifié, et nous ne pouvons pas nous le permettre.
Heureusement la sécurité des données est incluse dans les collections Meteor. Elle est désactivée par défaut lorsque vous créez un nouveau projet. Cela vous permet de commencer facilement à créer votre application tout en laissant les trucs ennuyeux pour plus tard.
Notre appli n'a plus besoin de ces petites roues, alors enlevons-les ! Supprimons le paquet `insecure` :
~~~bash
meteor remove insecure
~~~
<%= caption "Terminal" %>
Après avoir fait ça, vous remarquerez que le formulaire de post ne fonctionne plus. C'est parce que sans le paquet `insecure`, les `insert()` côté client sur la collection des posts *ne sont plus autorisés*.
Nous devons soit donner quelques règles explicites à Meteor pour définir quand un client est autorisé à insérer un post, soit faire nos insertions côté serveur.
### Autoriser les insertions de post
Pour commencer, nous allons voir comment autoriser les insertions de post côté client pour que notre formulaire fonctionne à nouveau. Il s'avère que nous finirons par adopter une technique différente, mais pour le moment, le simple code suivant permettra que les choses fonctionnent à nouveau :
~~~js
Posts = new Meteor.Collection('posts');
Posts.allow({
insert: function(userId, doc) {
// autoriser les posts seulement si l'utilisateur est authentifié
return !! userId;
}
});
~~~
<%= caption "collections/posts.js" %>
<%= highlight "3~8" %>
<%= commit "7-2", "Sans Insecure et avec des règles de modifications sur les posts ." %>
Nous appelons `Posts.allow`, qui dit à Meteor que "ceci est un ensemble de circonstances dans lesquelles les clients sont autorisés à faire des choses à la collection `Post`". Dans notre cas, nous disons "les clients peuvent insérer des posts du moment qu'ils ont un `userId`".
Le `userId` de l'utilisateur procédant à la modification est passé lors des appels à `allow` et `deny` (ou renvoie `null` si l'utilisateur n'est pas authentifié), ce qui est presque toujours utile. Et comme les comptes utilisateurs sont liés au noyau de Meteor, nous pouvons compter sur `userId` pour être toujours fiable.
Nous avons réussi à nous assurer que l'on doit être authentifié pour créer un post. Essayez de vous déconnecter et de créer un post ; vous devriez voir ceci dans votre console :
<%= screenshot "7-2", "Échec de l'insertion : Accès refusé " %>
Cependant, nous avons encore à nous occuper de quelques problèmes :
- Les utilisateurs non authentifiés peuvent toujours accéder au formulaire de création de post.
- Le post n'est pas lié à l'utilisateur de quelque façon que ce soit (et il n'y a pas de code côté serveur pour s'occuper de ça).
- Plusieurs posts peuvent être créés pointant vers la même URL.
Résolvons ces problèmes.
### Sécuriser l'accès au nouveau formulaire
Commençons par éviter l'accès des utilisateurs non authentifiés au formulaire d'ajout de post. Nous le ferons au niveau du routeur, en définissant un *route hook*.
Un *hook* ("crochet" en français) intercepte le processus de routage et change potentiellement l'action prise par le routeur. Vous pouvez l'imaginer comme un garde de sécurité qui vérifie vos identifiants avant de vous laisser entrer (ou de vous refuser l'accès).
Nous avons besoin de vérifier si l'utilisateur est authentifié, et sinon afficher le template `accessDenied` au lieu du `postSubmit` attendu (puis nous stoppons le routeur ici, il n'a rien d'autre à faire). Modifions donc router.js :
~~~js
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'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "17~23,26" %>
Nous créons aussi le template pour la page 'accès refusé', `accessDenied` :
~~~html
<template name="accessDenied">
<div class="access-denied page jumbotron">
<h2>Accès refusé</h2>
<p>Vous ne pouvez pas accéder à cette page ! Veuillez vous connecter.</p>
</div>
</template>
~~~
<%= caption "client/templates/includes/access_denied.html" %>
<%= commit "7-3", "Accès à la page d'ajout de post refusé lorsque non authentifié." %>
Maintenant, si vous allez à http://localhost:3000/submit/ sans être authentifié, vous devriez voir ceci :
<%= screenshot "7-3", "Le template d'accès refusé" %>
Ce qui est pratique à propos des hooks de routage, c'est qu'ils sont eux aussi *réactifs*. C'est-à-dire que nous n'avons pas besoin de penser à des callbacks quand l'utilisateur s'authentifie : quand l'état d'authentification de l'utilisateur change, le template utilisé par le routeur change instantanément de `accessDenied` à `postSubmit` sans que nous ayons rien à écrire d'explicite pour le gérer (et cela fonctionne d'ailleurs pour tous les onglets).
Authentifiez-vous, puis essayer de rafraîchir la page. Vous verrez peut-être parfois le template `accessDenied` s'afficher brièvement avant que la page d'ajout de post apparaisse. La raison en est que Meteor commence à afficher les templates dès que possible, avant même qu'il ait conversé avec le serveur est vérifié si l'utilisateur actuel (stocké dans la mémoire locale du navigateur) existe réellement.
Pour éviter ce problème (qui est courant et que vous rencontrerez d'autant plus que vous allez vous pencher sur les subtilités de latence entre client et serveur), nous allons simplement afficher un écran de chargement pendant que nous attendons de savoir si l'utilisateur est authentifié ou pas.
Après tout, à cet instant nous ne savons pas si l'utilisateur a les identifiants corrects, et nous ne pouvons afficher aucun des templates `accessDenied` ou `postSubmit` avant de le savoir.
Nous modifions donc notre hook pour utiliser notre template de chargement pendant que `Meteor.loggingIn()` est vrai :
~~~js
//...
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'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "5~10" %>
<%= commit "7-4", "Afficher un écran de chargement pendant l'authentification." %>
### Cacher le lien
La façon la plus simple d'éviter aux utilisateurs d'essayer d'atteindre cette page par erreur lorsqu'ils ne sont pas connectés est de leur cacher le lien. Nous pouvons faire cela facilement :
~~~html
//...
<ul class="nav navbar-nav">
{{#if currentUser}}<li><a href="{{pathFor 'postSubmit'}}">Créer un post</a></li>{{/if}}
</ul>
//...
~~~
<%= caption "client/templates/includes/header.html" %>
<%= highlight "3~5" %>
<%= commit "7-5", "Afficher le lien d'ajout de post seulement si authentifié." %>
Le helper `currentUser` est fourni par le paquet `accounts` et est l'équivalent Spacebars de `Meteor.user()`. Puisqu'il est réactif, le lien apparaîtra ou disparaîtra selon que vous vous connectez ou déconnectez de l'application.
### Meteor Method : une meilleure abstraction et sécurité
Nous avons réussi à sécuriser l'accès à la page d'ajout de post pour les utilisateurs non authentifiés, et éviter qu'ils puissent créer des posts même s'ils trichent en utilisant la console. Nous avons cependant encore plusieurs choses à faire :
- Marquer la date du post avec un *timestamp*
- S'assurer que la même URL ne peut pas être postée plus d'une fois
- Ajouter des détails à propos de l'auteur du post (ID, nom d'utilisateur, etc.)
Vous pourriez penser que nous pouvons faire tout cela dans notre gestionnaire de l'événement `submit`. Cependant, en réalité, nous arriverions vite à plusieurs problèmes :
- Pour le *timestamp*, nous aurions à espérer que l'heure soit correcte sur l'ordinateur de l'utilisateur, ce qui ne sera pas toujours le cas
- Le côté client ne sera pas au courant de _toutes_ les URL postées sur le site. Il connaîtra seulement les posts que l'utilisateur peut actuellement voir (nous verrons plus tard comment ceci fonctionne exactement). Il n'y a donc pas de moyen d'assurer l'unicité des URL du côté client.
- Enfin, même si nous _pourrions_ ajouter les détails utilisateurs côté client, nous ne nous assurerions pas de leur exactitude, ce qui pourrait exposer notre appli à une exploitation par des gens utilisant la console.
Pour toutes ces raisons, il vaut mieux garder nos gestionnaires d'événements simples. Et si nous faisons plus que les insertions et mises à jour les plus basiques, utilisons plutôt une **Méthode**.
Une Méthode Meteor est une fonction côté serveur qui est *appelée* côté client. Elles ne nous sont pas totalement étrangères -- en fait, en coulisses, les fonctions `insert`, `update`, et `remove` de `Collection` sont toutes des Méthodes. Voyons comment créer la nôtre.
Revenons à `post_submit.js`. Plutôt que d'insérer directement dans la collection `Posts`, nous allons appeler une Méthode nommée `postInsert` :
~~~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) {
// affiche l'erreur à l'utilisateur et s'interrompt
if (error)
return alert(error.reason);
Router.go('postPage', {_id: result._id});
});
}
});
~~~
<%= caption "client/templates/posts/post_submit.js" %>
<%= highlight "10~16" %>
La fonction `Meteor.call` appelle une Methode nommée par son premier argument. Vous pouvez fournir des arguments pour l'appel (ici, l'objet `post` construit depuis le formulaire), et finalement y attacher un callback, qui sera executé quand la Methode côté serveur sera terminé.
Les callbacks des méthodes meteor ont toujours deux arguments, `error` et `result`. Si pour une raison ou une autre, l'argument `error` existe, nous alerterons l'utilisateur (en utilisant `return` pour interrompre le callback). Si tout fonctionne normalement, nous redirigerons l'utilisateur vers la page de discussion du post fraîchement créée.
### Test de sécurité
Nous allons profiter de cette opportunité pour sécuriser notre méthode un peu plus en utilisant le paquet [`audit-argument-checks`](http://docs.meteor.com/#/full/auditargumentchecks).
Ce paquet vous permet de tester n'importe quel objet JavaScript selon un schéma prédéfini. Dans notre cas, nous l'utiliserons pour tester que l'utilisateur qui utilise la méthode est bien authentifié (en nous assurant que `Meteor.userId()` est une chaîne (`String`), et que l'objet `postAttributes` passé en tant qu'argument à la méthode contient les chaînes `title` et `url`, pour ne pas nous retrouver à entrer des morceaux aléatoires de données dans notre base de données.
Définissons donc la méthode `postInsert` dans notre fichier `collections/posts.js`. Nous supprimerons le bloc `allow()` de `posts.js` puisque de toute façon les méthodes Meteor les ignorent.
Nous allons ensuite étendre (`extend`) l'objet `postAttributes` avec trois nouvelles propriétés : l'`_id` et le nom (`username`) de l'utilisateur, ainsi que l'horodatage du post soumis (`submitted`), avant d'insérer le tout dans notre base de donnée et de retourner l'`_id` final au client (en d'autres mots, celui à l'origine de l'appel de cette méthode) dans un objet JavaScript.
~~~js
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
};
}
});
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "3~24" %>
Notez que la méthode `_.extend()` fait partie de la librairie [Underscore](http://underscorejs.org), et qu'elle vous permet simplement d'"étendre" un objet avec les propriétés d'un autre.
<%= commit "7-6", "En utilisant une méthode pour soumettre le post." %>
<% note do %>
### Bye Bye Allow/Deny
Les méthodes Meteor sont exécutées sur le serveur, et donc Meteor leur fait confiance. C'est pourquoi les méthodes Meteor ignorent n'importe quel callback allow/deny.
Si vous voulez exécuter du code avant chaque `insert`, `update`, ou `remove` *même sur le serveur*, nous vous suggérons de jeter un œil au paquet [collection-hooks](https://github.com/matb33/meteor-collection-hooks).
<% end %>
### Éviter les doublons
Nous ferons un dernier test avant d'intégrer notre méthode. Si un post avec la même url a déjà été créé auparavant, nous n'ajouterons pas le lien une nouvelle fois, mais au lieu de cela nous allons rediriger l'utilisateur vers ce post.
~~~js
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
};
}
});
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "9~15" %>
Nous cherchons dans notre base de données un post avec la même url. Si nous en trouvons un, nous retournons (`return`) l'`_id` de ce posts avec la propriété `postExists:true` pour informer le client de ce cette situation particulière.
Et puisque nous déclenchons un appel à `return`, la méthode s'interrompt à ce moment sans exécuter la déclaration `insert`, évitant ainsi avec élégance de créer des doublons.
Tout ce qui nous reste à faire est d'utiliser cette nouvelle information `postExists` dans notre helper d'événement côté client pour afficher un message d'avertissement :
~~~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) {
// affiche l'erreur à l'utilisateur et s'interrompt
if (error)
return alert(error.reason);
// affiche ce résultat mais route tout de même
if (result.postExists)
alert('Ce lien a déjà été utilisé');
Router.go('postPage', {_id: result._id});
});
}
});
~~~
<%= caption "client/templates/posts/post_submit.js" %>
<%= highlight "15~17" %>
<%= commit "7-7", "S'assurer de l'unicité de l'url du post." %>
### Trier les posts
Maintenant que nous avons une date de création sur tous nos posts, il semble bienvenu de s'assurer qu'ils soient classés en fonction de cet attribut. Pour cela, nous pouvons simplement utiliser l'opérateur `sort` de Mongo, qui attend un objet constitué des clés par lesquelles trier et un signe indiquant si le tri doit être croissant ou décroissant.
~~~js
Template.postsList.helpers({
posts: function() {
return Posts.find({}, {sort: {submitted: -1}});
}
});
~~~
<%= caption "client/templates/posts/posts_list.js" %>
<%= highlight "3" %>
<%= commit "7-8", "Tri des posts par date de création." %>
Ça a demandé un peu de travail, mais nous avons maintenant une interface utilisateur qui permet d'ajouter du contenu de façon sécurisée à notre appli !
Mais toute application qui permet à ses utilisateurs de créer du contenu doit aussi leur donner la possibilité de le modifier ou de le supprimer. Ce sera le sujet du prochain chapitre.