-
Notifications
You must be signed in to change notification settings - Fork 30
/
07-creating-posts.md.erb
485 lines (356 loc) · 20.5 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: Creando posts
slug: creating-posts
date: 0007/01/01
number: 7
points: 5
photoUrl: http://www.flickr.com/photos/markezell/9688179085
photoAuthor: Mark Ezell
contents: Aprenderemos a enviar posts desde el cliente.|Implementaremos un sencillo control de seguridad.|Restringiremos el acceso al formulario de envío.|Aprenderemos a utilizar métodos de servidor para mejorar la seguridad.
paragraphs: 60
---
Hemos visto lo fácil que es crear posts llamando a `Posts.insert` a través de la consola pero, no podemos esperar que nuestros usuarios hagan lo mismo.
Necesitamos construir algún tipo de interfaz de usuario para que los usuarios creen nuevas entradas en la aplicación.
### Creando la página de envío
Empezaremos definiendo una ruta para nuestra nueva página en `lib/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'});
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "15" %>
### Añadiendo un enlace en la cabecera
Con la ruta definida, ahora podemos añadir un enlace a la cabecera de nuestra página:
~~~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'}}">Submit Post</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{{> loginButtons}}
</ul>
</div>
</nav>
</template>
~~~
<%= caption "client/templates/includes/header.html" %>
<%= highlight "13~15" %>
Configurar una ruta implica que si un usuario navega a `/submit`, Meteor mostrará la plantilla `postSubmit`. Así que vamos a escribir esa plantilla:
~~~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="Your 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="Name your post" class="form-control"/>
</div>
</div>
<input type="submit" value="Submit" class="btn btn-primary"/>
</form>
</template>
~~~
<%= caption "client/templates/posts/post_submit.html" %>
Aquí hay un montón de markup, pero es solo porque usamos el CSS de Twitter Bootstrap. Aunque sólo son esenciales los elementos del formulario, el marcado adicional ayudará a que nuestra aplicación se vea un poco mejor. Ahora debería tener un aspecto similar a este:
<%= screenshot "7-1", "El formulario de creación de posts" %>
Es un simple formulario. No tenemos que preocuparnos de programar una acción para él, porque interceptaremos su evento `submit` y actualizaremos los datos vía JavaScript. (No tiene sentido proporcionar un fallback no-JS si tenemos en cuenta que Meteor no funciona con JavaScript desactivado).
### Creando posts
Vamos a enlazar un controlador de eventos al evento `submit` del formulario. Es mejor usar el evento `submit` (en lugar de un click en un botón), ya que cubrirá todas las posibles formas de envío (como por ejemplo pulsar intro).
~~~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", "Nueva página de envío y enlace a ella desde la cabecera." %>
Esta función utiliza [jQuery](http://jquery.com) para analizar los valores de los distintos campos del formulario y rellenar un objeto post con los resultados. Tenemos que asegurarnos de usar `preventDefault` para que el navegador no intente enviar el formulario si volvemos atrás o adelante después.
Al final, podemos dirigirnos a la página de nuestro nuevo post. La función `insert()` devuelve el identificador `_id` del objeto que se ha insertado en la base de datos, que podemos pasar a la función `go()` del router para que nos lleve a la página correcta.
El resultado es que el usuario pulsa en `submit`, se crea un nuevo post, y vamos inmediatamente a la página de discusión de ese nuevo post.
### Añadiendo algo de seguridad
Tal como está ahora, cualquiera que visite la web puede crear posts. Para evitarlo, debemos hacer que los usuarios inicien sesión. Podríamos ocultar el nuevo formulario, pero aún así, se podría seguir haciendo desde la consola.
Afortunadamente, Meteor gestiona la seguridad de las colecciones de la forma adecuada, lo que ocurre es que, por defecto, esta característica viene desactivada. Esto es así para permitirnos empezar con facilidad a construir la aplicación, dejando las cosas aburridas para más tarde.
Es el momento de eliminar el paquete `insecure`:
~~~bash
meteor remove insecure
~~~
<%= caption "Terminal" %>
Después de hacerlo, nos damos cuenta de que el formulario de posts ya no funciona. Esto es así, porque sin el paquete `insecure`, *no se permiten inserciones* en la colección de posts desde el lado del cliente.
Necesitamos escribir reglas explícitas para decirle a Meteor qué usuarios pueden insertar posts o hacer que las inserciones se hagan en el lado del servidor.
### Permitir insertar posts
Para que nuestro formulario funcione de nuevo, vamos a ver cómo permitir posts del lado del cliente. Como veremos, al final usaremos una técnica diferente, pero por ahora, lo pondremos todo a funcionar de nuevo, de una forma sencilla: en `collections/posts.js`:
~~~js
Posts = new Mongo.Collection('posts');
Posts.allow({
insert: function(userId, doc) {
// only allow posting if you are logged in
return !! userId;
}
});
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "3~8" %>
<%= commit "7-2", "Eliminado el paquete `insecure` y permitido añadir posts a usuarios registrados." %>
Llamamos a `Posts.allow`, que le dice a Meteor que "se trata de un conjunto de circunstancias en las que a los clientes se les permite hacer cosas en la colección de `Posts`". En este caso, estamos diciendo: "a los clientes se les permite insertar posts siempre y cuando tengan un `userId`".
El `userId` que realiza la modificación se pasa a las funciones `allow` y `deny` (o devuelve `null` si no hay ningún usuario conectado). Como las cuentas de usuario forman parte del núcleo de Meteor, podemos confiar en que el `userId` siempre será el correcto.
Nos las hemos arreglado para asegurarnos de que un usuario tiene que estar registrado para crear un mensaje. Salimos de la sesión e intentamos crear un post para ver lo que sale por la consola del navegador:
<%= screenshot "7-2", "Error en la inserción: Acceso denegado." %>
Sin embargo, todavía tenemos que tratar con unas cuantas cosas:
- Los usuarios que no han iniciado sesión aún pueden ver el formulario.
- El post no está vinculado al usuario de ninguna forma.
- Se pueden crear múltiples posts que apunten a la misma URL.
Vamos a corregir estos problemas.
### Asegurar el acceso al formulario
Vamos a empezar por evitar que los usuarios no registrados puedan ver el formulario de envío de posts. Lo haremos a nivel de router, definiendo una acción (_hook_) del router.
Una acción intercepta el proceso de enrutamiento y, potencialmente, cambia la acción que lleva acabo el router. Puedes pensar en él como en un guardia de seguridad que verifica tus credenciales antes de dejarte entrar.
Lo que tenemos que hacer es comprobar si el usuario está conectado. Si no lo está, mostramos la plantilla `accessDenied` en lugar de la plantilla `postSubmit` (en este momento le diremos al router que no haga nada más). Así que vamos a modificar `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" %>
Además, tenemos que crear una plantilla para la página de error:
~~~html
<template name="accessDenied">
<div class="access-denied page jumbotron">
<h2>Access Denied</h2>
<p>You can't get here! Please log in.</p>
</div>
</template>
~~~
<%= caption "client/templates/includes/access_denied.html" %>
<%= commit "7-3", "Acceso denegado al envío de posts a usuarios no registrados." %>
Si ahora nos dirigimos a [http://localhost:3000/submit/](http://localhost:3000/submit/) sin estar registrados, veremos el mensaje de error:
<%= screenshot "7-3", "Plantilla de error de acceso" %>
Lo bueno de las acciones del router es que son *reactivas*. Esto significa que no necesitamos pensar en funciones de retorno cuando el usuario se autentica: cuando el estado de autenticación del usuario cambia, la plantilla del Router cambia instantáneamente de `accessDenied` a `postSubmit` sin tener que escribir explícitamente código para manejarlo (y además, esto funciona incluso en las otras pestañas del navegador).
Iniciemos sesión, y vayamos a la página para crear un nuevo post. Ahora actualizar la página en el navegador. Veremos que, por un instante, se ve la plantilla `accessDenied` antes de que aparezca el formulario. Esto es porque Meteor empieza a mostrar las plantillas tan pronto como sea posible, antes de haber hablado con el servidor y comprobado si el usuario existe.
Para evitar este problema (que es uno de los más comunes que nos podemos encontrar cuando tratamos de lidiar con la latencia entre el cliente y el servidor), solo mostraremos una pantalla de espera durante un instante en el que esperamos para ver si el usuario tiene acceso o no.
Después de todo en este momento no sabemos si el usuario tiene acceso y no podemos mostrar ninguna de las plantillas, ya sea la de `accessDenied` o la de `postSubmit` hasta que lo sepamos.
Así que vamos a modificar nuestra acción para añadir la plantilla de espera mientras `Meteor.loggingIn()` sea verdadero en:
~~~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~9" %>
<%= commit "7-4", "Mostrar la pantalla de carga mientras esperamos al login." %>
### Ocultando el enlace
La forma fácil de evitar que los usuarios lleguen al formulario es esconder el enlace. Podemos hacerlo fácilmente desde `header.html`:
~~~html
//...
<ul class="nav navbar-nav">
{{#if currentUser}}<li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>{{/if}}
</ul>
//...
~~~
<%= caption "client/templates/includes/header.html" %>
<%= highlight "3~5" %>
<%= commit "7-5", "No mostrar el enlace a la página de envío si el usuario no se ha identificado." %>
El paquete `accounts` nos ofrece el ayudante `currentUser` que es el equivalente a `Meteor.user()` en Spacebars. Puesto que es reactivo, el enlace aparecerá o desaparecerá según el estado del usuario.
### Meteor.methods para mejorar la seguridad y la abstracción
Nos las hemos arreglado para asegurar el acceso a la página de entrada de posts, y no permitir crear posts a usuarios no registrados incluso si intentan hacerlo desde la consola. Sin embargo, todavía quedan cosas que debemos mejorar:
- Añadir el timestamp de los posts.
- Asegurarse de que no hay URLs duplicadas.
- Añadir detalles sobre el autor del post (ID, nombre de usuario, etc.)
Podríamos pensar en hacer todo esto en nuestro controlador `submit`. Pero, haciéndolo de esta forma, nos encontraríamos con un montón de problemas.
- Para el timestamp, tendríamos que confiar en la hora de la máquina del usuario.
- Los clientes no conocerán `todas` las URL publicadas. Solo conocen los posts que pueden ver en ese momento (veremos porqué), así que no podemos asegurar desde el lado del cliente que las URLs sean únicas.
- Por último, aunque _podríamos_ añadir la información de usuario en el lado del cliente, estaríamos abriendo nuestra aplicación a posibles ataques de usuarios usando la consola del navegador.
Por todas estas razones, es mejor mantener nuestros controladores de eventos simples y, si queremos hacer más inserciones o actualizaciones en las colecciones, debemos usar **métodos**.
Un método en Meteor es una función del lado del servidor que *se llama* desde el lado del cliente. Ya estamos familiarizados con ellos -- de hecho, entre bastidores, la inserción, la actualización y el borrado de datos de la colección, son métodos. Vamos a ver cómo crear el nuestro.
Volvamos a `post_submit.js`. En lugar de insertar directamente en la colección `Posts`, vamos a llamar a un método llamado `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) {
// display the error to the user and abort
if (error)
return alert(error.reason);
Router.go('postPage', {_id: result._id});
});
}
});
~~~
<%= caption "client/templates/posts/post_submit.js" %>
<%= highlight "10~16" %>
La función `Meteor.call` llama a un método nombrado por su primer argumento. Se pueden proporcionar argumentos a la llamada (en este caso, pasamos el objeto `post` que hemos construido del formulario), y, finalmente, habilitamos un callback, que se ejecutará cuando el método del lado del servidor finalice.
Las funciones de retorno de los métodos Meteor siempre tienen dos argumentos, `error` y `result`. Si por cualquier razón el argumento `error` existe, avisaremos al usuario (usando `return` para finalizar la función). Si todo ha funcionado bien, redirigiremos al usuario a la página de discusión del post recién creado.
### Comprobaciones de seguridad
Aprovecharemos esta oportunidad para añadir algo de seguridad a nuestros métodos usando el paquete [`audit-argument-checks`](http://docs.meteor.com/#/full/auditargumentchecks).
Este paquete nos permite realizar comprobaciones sobre un objeto JavaScript usando patrones predefinidos. En nuestro caso, lo usaremos para comprobar que el usuario que está invocando el método está correctamente autenticado (asegurándonos que `Meteor.userId()` es de tipo `String`), y que el objeto `postAttributes` pasado como argumento al método contiene las cadenas `title` y `url`, para no terminar insertando cualquier dato extraño en nuestra base de datos.
Vamos a definir el método `postInsert` en nuestro fichero `collections/posts.js`. Eliminaremos el bloque `allow()` del fichero `posts.js` porque usando métodos, Meteor no lo evalúa.
Extenderemos (`extend`) el objeto `postAttributes` con tres propiedades más: el identificador del usuario `_id` y el `username`, además de la fecha y hora `submitted`, antes de insertarlos en nuestra base de datos y devolver el `_id` al cliente (en otras palabras, a la función original que llamó a este método) como un objeto 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" %>
Fíjate que el método `_.extend()` forma parte de la librería [Underscore](http://underscorejs.org), que simplemente nos permite “extender” un objeto con propiedades de otro.
<%= commit "7-6", "Usando un método para enviar un post." %>
<% note do %>
### Adiós Allow/Deny
Los métodos Meteor son ejecutados en el servidor, por lo que Meteor supone que son de confianza. Por tanto, los métodos Meteor obvian las llamadas a `allow` y `deny`.
Si quieres ejecutar algún código antes de cada operación de `insert`, `update`, o `remove` *incluso en el lado servidor*, te sugerimos echar un vistazo al paquete [collection-hooks](https://github.com/matb33/meteor-collection-hooks).
<% end %>
### Evitando duplicidades
Vamos a hacer una comprobación más antes de dar por bueno nuestro método. Si ya tenemos un post con la misma URL, no vamos a permitir que se añada una segunda vez, por el contrario, redirijamos al usuario al post ya existente.
~~~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" %>
Buscamos en nuestra base de datos las URLs duplicadas. Si se encuentra alguna, devolvemos (`return`) el `_id` del post junto con una marca `postExists: true` para informar al cliente sobre esta situación especial.
Y como estamos lanzando una llamada `return`, el método se detiene en este punto sin llegar a ejecutar la sentencia `insert`, evitándonos elegantemente cualquier duplicidad.
Sólo falta usar `postExists` en nuestro ayudante de eventos en el lado del cliente para mostrarnos un mensaje de aviso:
~~~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) {
// display the error to the user and abort
if (error)
return alert(error.reason);
// show this result but route anyway
if (result.postExists)
alert('This link has already been posted');
Router.go('postPage', {_id: result._id});
});
}
});
~~~
<%= caption "client/templates/posts/post_submit.js" %>
<%= highlight "15~17" %>
<%= commit "7-7", "Forzando la unicidad de las URLs." %>
### Ordenando los posts
Ahora que tenemos una fecha de envío en todos nuestros posts, tiene sentido asegurarnos que se están ordenando usando este atributo. Para ello usaremos el operador `sort` de Mongo que espera un objeto que consta de las claves de ordenación, y un signo que indica si son ascendentes o descendentes:
~~~js
Template.postsList.helpers({
posts: function() {
return Posts.find({}, {sort: {submitted: -1}});
}
});
~~~
<%= caption "client/templates/posts/posts_list.js" %>
<%= highlight "3" %>
<%= commit "7-8", "Posts ordenados por fecha de envío." %>
Ha costado, pero ¡Finalmente tenemos una interfaz en la que los usuarios introducen posts de forma segura en nuestra aplicación!
Sin embargo, cualquier aplicación que permita a los usuarios crear contenido también debe permitir editarla o borrarla. Eso es de lo que hablaremos en el siguiente capítulo.