forked from DiscoverMeteor/DiscoverMeteor_fr
-
Notifications
You must be signed in to change notification settings - Fork 0
/
07-creating-posts.md.erb
429 lines (313 loc) · 21 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
---
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',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.map(function() {
this.route('postsList', {path: '/'});
this.route('postPage', {
path: '/posts/:_id',
data: function() { return Posts.findOne(this.params._id); }
});
this.route('postSubmit', {
path: '/submit'
});
});
Router.onBeforeAction('loading');
~~~
<%= caption "lib/router.js" %>
<%= highlight "13~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">
<header class="navbar">
<div class="navbar-inner">
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
<div class="nav-collapse collapse">
<ul class="nav">
<li><a href="{{pathFor 'postSubmit'}}">New</a></li>
</ul>
<ul class="nav pull-right">
<li>{{loginButtons}}</li>
</ul>
</div>
</div>
</header>
</template>
~~~
<%= caption "client/views/includes/header.html" %>
<%= highlight "11~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">
<div class="control-group">
<label class="control-label" for="url">URL</label>
<div class="controls">
<input name="url" type="text" value="" placeholder="Your URL"/>
</div>
</div>
<div class="control-group">
<label class="control-label" for="title">Title</label>
<div class="controls">
<input name="title" type="text" value="" placeholder="Name your post"/>
</div>
</div>
<div class="control-group">
<label class="control-label" for="message">Message</label>
<div class="controls">
<textarea name="message" type="text" value=""></textarea>
</div>
</div>
<div class="control-group">
<div class="controls">
<input type="submit" value="Submit" class="btn btn-primary"/>
</div>
</div>
</form>
</template>
~~~
<%= caption "client/views/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", "The post submit form" %>
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 est complètement non-fonctionnelle 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 que 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 dans le champ URL 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(),
message: $(e.target).find('[name=message]').val()
}
post._id = Posts.insert(post);
Router.go('postPage', post);
}
});
~~~
<%= caption "client/views/posts/post_submit.js" %>
<%= commit "7-1", "Ajouté une page d'ajout de post et fait un lien vers elle 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.
Au final, l'utilisateur envoie le formulaire, un nouveau post est créé, et l'utilisateur est instantanément dirigé vers la page de discussion pour 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 avec leurs identifiants 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. Elles est désactivée par défaut lorsque vous créez un nouveau projet. Cela vous permet de commencer facilement et commencez à 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 ! Nous enlevons 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 finiront 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", "Enlevé insecure et autorisé certaines 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é). Et comme les comptes utilisateurs sont liés au noyau de Meteor, nous pouvons compter sur `userId` pour toujours être toujours disponible. Maintenant, si vous allez à http://localhost:3000/submit/ sans être authentifié, vous devriez voir ceci :
Nous avons réussi à nous assurer que l'orefusé n 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 :Maintenant, si vous allez à http://localhost:3000/submit/ sans être authentifié, vous devriez voir ceci :
<%= 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.
- 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*.Maintenant, si vous allez à http://localhost:3000/submit/ sans être authentifié, vous devriez voir ceci :
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 pas autre chose à faire). Modifions router.js ainsi Maintenant, si vous allez à http://localhost:3000/submit/ sans être authentifié, vous devriez voir ceci :
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.map(function() {
this.route('postsList', {path: '/'});
this.route('postPage', {
path: '/posts/:_id',
data: function() { return Posts.findOne(this.params._id); }
});
this.route('postSubmit', {
path: '/submit'
});
});
var requireLogin = function(pause) {
if (! Meteor.user()) {
this.render('accessDenied');
pause();
}
}
Router.onBeforeAction('loading');
Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "19~26,29" %>
Nous créons aussi le template pour l'accès refusé, `accessDenied` :
~~~html
<template name="accessDenied">
<div class="alert alert-error">You can't get here! Please log in.</div>
</template>
~~~
<%= caption "client/views/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 _réactifs_. C'est-à-dire que nous pouvons être déclaratifs et n'avons pas besoin de penser à des callbacks ou autres 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`. Et ceci sans que nous ayons rien à écrire d'explicite pour le gérer.
Authentifiez-vous, puis essayer de rafraîchir la page. Vous verriez peut-être 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 est conversé avec le serveur est vérifié si l'utilisateur (stocké dans la mémoire locale du navigateur) existe.
C'est un genre de problème courant que vous allez rencontrer lorsque vous vous penchez sur les subtilités de la compensation de latence entre client et serveur. Pour l'éviter, 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 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 ("logging in" signifie "en cours d'authentification") :
~~~js
Router.map(function() {
this.route('postsList', {path: '/'});
this.route('postPage', {
path: '/posts/:_id',
data: function() { return Posts.findOne(this.params._id); }
});
this.route('postSubmit', {
path: '/submit'
});
});
var requireLogin = function(pause) {
if (! Meteor.user()) {
if (Meteor.loggingIn())
this.render(this.loadingTemplate);
else
this.render('accessDenied');
pause();
}
}
Router.onBeforeAction('loading');
Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "16~19" %>
<%= 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">
{{#if currentUser}}<li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>{{/if}}
</ul>
~~~
<%= caption "client/views/includes/header.html" %>
<%= 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 alors 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 **Method**.
Une Method (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 Methods. 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 Method nommée `post` :
~~~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(),
message: $(e.target).find('[name=message]').val()
}
Meteor.call('post', post, function(error, id) {
if (error)
return alert(error.reason);
Router.go('postPage', {_id: id});
});
}
});
~~~
<%= caption "client/views/posts/post_submit.js" %>
La fonction `Meteor.call` appelle une Method nommée par son premier argument (`'post'`). 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 Method côté serveur a terminé. Ici nous alertons simplement l'utilisateur s'il y a un problème, ou bien le redirigeons vers la page de discussion du post fraîchement créé.
Nous définissons ensuite la Method dans notre fichier `collections/posts.js`. Nous allons supprimer le bloc `allow()` de `posts.js` puisque les Méthodes Meteor les contournent de toute façon. Rappelez-vous que les Methods sont éxecutées sur le serveur, Meteor les considèrent donc de confiance.
~~~js
Posts = new Meteor.Collection('posts');
Meteor.methods({
post: function(postAttributes) {
var user = Meteor.user(),
postWithSameLink = Posts.findOne({url: postAttributes.url});
// assurons-nous que l'utilisateur est authentifié
if (!user)
throw new Meteor.Error(401, "You need to login to post new stories");
// que le post a un titre
if (!postAttributes.title)
throw new Meteor.Error(422, 'Please fill in a headline');
// vérifions qu'il n'y pas d'autre post avec le même lien
if (postAttributes.url && postWithSameLink) {
throw new Meteor.Error(302,
'This link has already been posted',
postWithSameLink._id);
}
// filtrons pour prendre les attributs attendus
var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
userId: user._id,
author: user.username,
submitted: new Date().getTime()
});
var postId = Posts.insert(post);
return postId;
}
});
~~~
<%= caption "collections/posts.js" %>
<%= commit "7-6", "Utiliser une Method pour envoyer le post." %>
Cette Method est un peu compliquée, mais vous pouvez sûrement nous suivre.
Premièrement, nous définissons notre variable `user` et vérifions si un post avec le même lien existe déjà. Après cela, nous vérifions que l'utilisateur est authentifié, et lançons (avec `throw`) une erreur si ce n'est pas le cas (l'utilisateur en sera alors `alert`-é par son navigateur). Nous faisons également une validation simple pour s'assurer que le post a un titre.
Ensuite, s'il existe un autre post avec la même URL, nous lançons une erreur `302` (redirection) signifiant à l'utilisateur qu'il devrait aller jeter un œil à ce post précédemment créé.
La class `Error` de Meteor prend trois arguments. Le premier (`error`) est un code d'erreur de votre choix (ici nous prenons `302`), le second (`reason`) est une explication courte et lisible de l'erreur, et le dernier (`details`) peut être toute information utile supplémentaire.
Dans notre cas, nous utilisons ce troisième argument pour passer l'ID du post que venons de trouver. Spoiler : nous utiliserons cela plus tard pour rediriger l'utilisateur vers le post pré-existant.
Si toutes ces vérifications passent, nous prenons les champs que nous voulons insérer en utilisant [pick](http://underscorejs.org/#pick) d'[Underscore](http://underscorejs.org/#pick) (pour nous assurer qu'un utilisateur appelant cette Method depuis la console du navigateur ne puisse pas insérer des données contrefaites), et incluons quelques informations sur l'utilisateur -- et la date actuelle -- dans le post en utilisant [extend](http://underscorejs.org/#extend).
Finalement, nous insérons le post, et retournons l'`id` du nouveau post à l'utilisateur.
### 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 d'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/views/posts/posts_list.js" %>
<%= highlight "3" %>
<%= commit "7-7", "Tri des posts par date de création." %>
Ça a demandé un peu de travail, mais nous avons maintenant une interface utilisateur qui 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 chapitre Éditer les posts.