forked from DiscoverMeteor/DiscoverMeteor_Pt
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path07-creating-posts.md.erb
423 lines (307 loc) · 19.4 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
---
title: Criando Artigos
slug: creating-posts
date: 0007/01/01
number: 7
contents: Aprenda como enviar um artigo no lado do cliente.|Implemente uma simples checagem de segurança.|Restrinja acesso ao formulário de envio de artigos.|Aprenda como usar Method do lado do servidor para adicionar segurança.
paragraphs: 60
---
Nós vimos como é fácil criar artigos através do console, usando a chamada `Posts.insert` ao banco de dados, mas nós não podemos esperar que nossos usuários abram o console para criar um novo artigo.
Eventualmente, nós precisaremos construir alguma forma de interface do usuário para permitir aos nossos usuários postar novos artigos no nosso aplicativo.
### Construindo uma Nova Página de Artigo
Nós começamos por definir a rota para a nossa nova página:
~~~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'
});
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "13~15" %>
Nós estamos usando a função `data` do roteador para configurar um contexto de informação do template `postPage`. Lembre-se que o quer que ponhamos nesse contexto de informação estará disponível ao `this` de dentro dos ajudantes de template.
### Adicionando um Link ao Cabeçalho
Com essa rota definida, nós podemos agora adicionar um link a nossa página de envio no nosso cabeçalho:
~~~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" %>
Configurar a nossa rota signifca que se o usuário procurar pela URL `/submit`, o Meteor mostrará o template `postSubmit`. Então vamos escrever esse 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=""/>
</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: isto é um monte de marcação, mas ela simplesmente advém de se usar o Twitter Bootstrap. Apesar de apenas os elementos de formulário serem essenciais, a marcação extra ajudará o nosso aplicativo a parecer um pouco melhor. Agora ele deverá se parecer com algo assim:
<%= screenshot "7-1", "The post submit form" %>
Este é um simples formulário. Nós não precisamos nos preocupar com a ação para ele, já que nós estaremos interceptando eventos de envio no formulário e atualizando a informação via JavaScript. (Não faz sentido prover um plano reserva sem ser em JS quando se considera que um aplicativo em Meteor é completamente não funcional quando o JavaScript está desativado).
### Criando Artigos
Vamos ligar um manuseador de evento ao evento `submit` do formulário. É melhor usar o evento `submit` (ao invés de digamos um evento `click` no botão), já que ele cobrirá todas maneiras possíves de envio (tais como apertar enter no campo da URL por exemplo).
~~~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", "Added a submit post page and linked to it in the header." %>
Esta função usa [jQuery](http://jquery.com) para analisar os valores dos vários campos do formulário, e povoar o novo objeto artigo com os resultados. Nós precisamos assegurar que nós `preventDefault` no argumento do `event` do nosso manuseador para garantir que o navegador não vá adiante e tente enviar o formulário.
Finalmente, nós podemos redirecionar a nossa nova página do artigo. A função `insert()` numa coleção retorna a `id` gerada para o objeto que foi inserido no banco de dados, a qual a função `go()` do Roteador usará para construir a URL para nós navegarmos até.
O resultado em rede é que o usuário aperta enviar, o artigo é criado, e o usuário é instantaneamente levado à página de discussão para este novo artigo.
### Adicionando Alguma Segurança
Criar artigos está todo certo, mas nós não queremos permitir que visitantes aleatórios o façam: nós queremos que eles precisem estar logados para tanto. Claro, nós podemos começar escondendo dos usuários não logados o formulário de novo artigo. Mesmo assim, um usuário poderia concebivelmente criar um artigo no console do navegador sem estar logado, e nós não queremos isso.
Agradecidamente a segurança de informação está inserida diretamente nas coleções Meteor; a questão é que ela fica desligada por padrão quando você cria um novo projeto. Isso permite que você comece facilmente e construa seu aplicativo deixando a parte chata para mais tarde.
Nosso aplicativo não precisa mais de rodinhas, então vamos tirá-las! Nós removeremos o pacote `insecure`:
~~~bash
$ meteor remove insecure
~~~
<%= caption "Terminal" %>
Após o fazê-lo, nocê notará que o formulário de artigo não funciona mais. Isto é porque sem o pacote `insecure`, inserções do lado do cliente na coleção posts _não são mais permitidas_. Nós precisamos ou dar regras explícitas ao Meteor sobre quando é OK para um cliente inserir artigos, ou fazer todas nossas inserções de artigo pelo lado do servidor.
### Permitindo Inserções de Artigo
Para começar, nós mostraremos como permitir inserções de artigos pelo lado do cliente para deixar nosso formulário funcionando de novo. Como de costume, nós eventualmente terminaremos com uma técnica diferente, mas por hora, o seguinte fará as coisas funcionarem de novo:
~~~js
Posts = new Meteor.Collection('posts');
Posts.allow({
insert: function(userId, doc) {
// only allow posting if you are logged in
return !! userId;
}
});
~~~
<%= caption "collections/posts.js" %>
<%= highlight "3~8" %>
<%= commit "7-2", "Removed insecure, and allowed certain writes to posts." %>
Nós chamamos `Posts.allow`, que diz ao Meteor "este é um conjunto de circunstâncias onde os clientes estão permitidos a fazer coisas à coleção `Posts`". Neste caso, nós estamos dizendo "clientes têm permissão de inserir artigos desde que eles tenham uma `userId`".
A `userId` do usuário fazendo a modificação é passada às chamadas `allow` and `deny` (ou retorna `null` se nenhum usuário estiver logado), o que é quase sempre útil. E como as contas de usuários são amarradas ao centro do Meteor, nós podemos contar com a `userId` estar sempre correta.
Nós conseguimos garantir que você precisará estar logado para criar um artigo. Tente deslogar e criar um artigo; você deve ver isto no console:
<%= screenshot "7-2", "Insert failed: Access denied " %>
Entretanto, nós ainda temos de lidar com algumas questões:
- Usuários não logados ainda conseguem alcançar o formulário de criação de artigo.
- O artigo não está ligado ao usuário de forma alguma (e não há código no servidor que garanta isso).
- Múltiplos artigos podem ser criados que apontem para a mesma URL.
Vamos consertar esses problemas.
### Protegendo o Acesso ao Formulário de Novo Artigo
Vamos começar prevenindo usuários não logados de ver o formulário de envio de artigo. Nós faremos isso no nível do roteador, definindo um *gancho de rota*.
Um gancho intercepta um processo de roteamento e potencialmente muda a ação que o roteador toma. Você pode pensá-lo como um segurança que checa suas credenciais antes de permitir que você entre (ou te recusar).
O que nós precisamos fazer é checar se o usuário está logado, e se eles não estiverem renderizar o template `accessDenied` ao invés do esperado template `postSubmit` (nós então paramos o roteador de fazer qualquer outra coisa). Vamos modificar o router.js para tanto:
~~~js
Router.configure({
layoutTemplate: 'layout'
});
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() {
if (! Meteor.user()) {
this.render('accessDenied');
this.stop();
}
}
Router.before(requireLogin, {only: 'postSubmit'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "18~25" %>
Nós também criamos o template da página acesso negado:
~~~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", "Denied access to new posts page when not logged in." %>
Se você tentar ir a http://localhost:3000/submit/ sem estar logado, você deve ver isto:
<%= screenshot "7-3", "The access denied template" %>
A coisa legal quanto a ganchos de roteamento é que eles são _reativos_. Isso signfica que nós podemos ser declarativos e não precisamos pensar em callbacks, ou similares, quando o usuário loga. Quando o estado de log-in do usuário muda, a página de template do Roteador muda instantaneamente de `accessDenied` para `postSubmit` sem que nós precisemos escrever nenhum código explícito para tanto.
Logue, e então tente atualizar a página. Você pode ver às vezes o template negado piscar por um breve momento antes da página de envio de artigo aparecer. A razão para tanto é que o Meteor começa renderizando templates o mais cedo possível, antes de conversar com o servidor e checar se o usuário atualmente (armazenado no banco local do navegador) sequer existe.
Para evitar este problema (que é uma classe de problemas comum que você verá mais quando você lidar com os detalhes da latência entre cliente e servidor), nós apenas mostraremos uma tela de loading pelo breve momento em que nós esperamos para ver se o usuário tem acesso ou não.
Até mesmo porque neste estágio nós não sabemos se o usário tem as credenciais log-in corretas, e nós não podemos mostrar tanto `accessDenied` ou o template `postSubmit` até que saibamos.
Então nós modificamos no nosso gancho para usar o nosso template loading embora `Meteor.loggingIn()` seja verdadeiro:
~~~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() {
if (! Meteor.user()) {
if (Meteor.loggingIn())
this.render(this.loadingTemplate);
else
this.render('accessDenied');
this.stop();
}
}
Router.before(requireLogin, {only: 'postSubmit'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "16~19" %>
<%= commit "7-4", "Show a loading screen while waiting to login." %>
### Escondendo o Link
A maneira mais fácil de prevenir que os usuários cheguem a esta página por engano quando eles não estão logados é esconder o link deles. Nós podemos fazer isso bem facilmente:
~~~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", "Only show submit post link if logged in." %>
O ajudante `currentUser` nos é provido pelo pacote `accounts` e é o handlebar equivalent de `Meteor.user()`. Já que é reativo, o link irá aparecer ou desaparecer ao você logar e deslogar do aplicativo.
### Método Meteor: Abstração e Segurança Melhores
Nós conseguimos proteger o acesso à página de novo artigo de usuário não logados, e negar que tais usuários criem artigos mesmo que eles trapaceiem e usem o console. Porém ainda há mais algumas coisas que nós precisamos tomar conta:
- Timestamping os artigos.
- Assegurar que uma mesma URL não seja publicada mais que uma vez.
- Adicionar detalhes sobre o autor do artigo (ID, username, etc.).
Você talvez esteja pensando que nós podemos fazer tudo isso no nosso manuseador de evento `submit`, entretanto, nós rapidamente encontraríamos uma série de problemas:
- Para a timestamp, nós teríamos de contar com a corretude do tempo do computador do usuário, o qual nem sempre será o caso.
- Clientes não saberam de _todas_ as URLs já publicadas no site. Eles apenas saberão dos artigos que eles podem ver atualmente (nós veremos como isso funciona exatamente mais tarde), então não há como garantir originalidade de URL pelo lado do cliente.
- Finalmente, apesar que nós _poderíamos_ adicionar detalhes de usuário pelo lado do cliente, nós não poderíamos garantir sua acurácia, o que poderia abrir o nosso aplicativo para os usuários trapaceando pelo console do navegador.
Por todas essas razões, é melhor manter nossos manuseadores de evento simples e, se nós estivermos fazendo mais do que inserções e atualizações básicas à coleção, usemos um **Método**.
Um Método Meteor é uma função do lado do servidor que é chamada pelo lado do cliente. Nós temos alguma familiaridade com elas -- aliás, por trás das cortinas, as funções `insert`, `update` e `remove` da `Coleção` são todos Métodos. Vamos ver como criar o nosso próprio.
Vamos voltar a `post_submit.js`. Ao invés de inserir diretamente na coleção `Posts`, nós chamaremos um Método chamado `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" %>
A função `Meteor.call` chama um Método nomeado pelo seu primeiro argumento. Você pode prover argumentos à chamada (neste caso, o objeto `post` que nós construímos através do formulário), e finalmente anexar um callback, o qual executará quando o Método do lado do servidor estiver finalizado. Aqui nós simplesmente alertamos o usuário se houve algum problema, ou redirecionamos o usuário à recentemente criada página de discussão do artigo caso contrário.
Nós definimos o Método no nosso arquivo `collections/posts.js`. Nós removeremos o bloco `allow()` do `posts.js` já que o Método Meteor ignora eles mesmo. Lembre-se que Métodos são executados no servidor, então o Meteor assume que eles sejam confiáveis.
~~~js
Posts = new Meteor.Collection('posts');
Meteor.methods({
post: function(postAttributes) {
var user = Meteor.user(),
postWithSameLink = Posts.findOne({url: postAttributes.url});
// ensure the user is logged in
if (!user)
throw new Meteor.Error(401, "You need to login to post new stories");
// ensure the post has a title
if (!postAttributes.title)
throw new Meteor.Error(422, 'Please fill in a headline');
// check that there are no previous posts with the same link
if (postAttributes.url && postWithSameLink) {
throw new Meteor.Error(302,
'This link has already been posted',
postWithSameLink._id);
}
// pick out the whitelisted keys
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", "Use a method to submit the post." %>
Este Método é um pouco complicado, mas felizmente você pode seguir adiante.
Primeiro, nós definimos nossa variável `user` e checamos se o artigo com o mesmo link já existe. Então, nós checamos para ver se o usuário está logado, jogando um erro (o qual eventualmente será `alert`-ado pelo navegador) caso contrário. Nós também fazemos alguma validação simples do objeto artigo para garantir que os nossos artigos tenham título.
Em seguida, se há outro artigo com a mesma URL, nós podemos lançar um erro `302` (o qual significa redirecionar) dizendo ao usuário que nós deveríamos ir e ver este artigo prévio.
A classe `Error` do Meteor recebe três argumentos. O primeiro (`error`) será o código numérico `302`, o segundo (`reason`) é uma pequena explicação legível por humanos do erro, e o último (`details`) pode ser qualquer informação útil adicional.
No nosso caso, nós usaremos este terceiro argumento para passar a ID do artigo que nós acabamos de encontrar. Spoiler alert: nós usaremos isso mais tarde para redirecionar o usuário para o artigo pré-existente.
Se todas essas checagens passarem, nós agarraremos os campos que nós queremos inserir (para garantir que o usuário que esteja chamando este Método não possa por informação falsa no nosso banco de dados), e incluiremos alguma informação sobre o usuário que envia -- assim como o tempo atual -- ao artigo.
Finalmente, nós inserimos o artigo, e retornamos a `id` do novo artigo ao usuário.
### Organizando Artigos
Agora que nós temos data de envio em todos os nossos artigos, faz sentido assegurar que eles estejam organizados usando esse atributo. Para tanto, nós podemos usar o operador `sort` do Mongo, que espera um objeto feito de chaves pelas quais organizar, e um sinal indicando se de forma ascendente ou descendente.
~~~js
Template.postsList.helpers({
posts: function() {
return Posts.find({}, {sort: {submitted: -1}});
}
});
~~~
<%= caption "client/views/posts/posts_list.js" %>
<%= highlight "3" %>
<%= commit "7-7", "Sort posts by submitted timestamp." %>
Levou um pouco de trabalho, mas nós finalmente temos uma interface do usuário para deixar nossos usuários adicionar conteúdo seguramente ao nosso aplicativo!
Mas qualquer aplicativo que permite ao usuário criar conteúdo também precisa deixá-los editá-lo e deletá-lo. Sobre isso que se tratará o capítulo Editando Artigos.