forked from DiscoverMeteor/DiscoverMeteor_Pl
-
Notifications
You must be signed in to change notification settings - Fork 0
/
07-creating-posts.md.erb
423 lines (307 loc) · 19.6 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: Tworzenie postów
slug: creating-posts
date: 0007/01/01
number: 7
contents: jak dodawać posty po stronie klienta.|jak zaimplementować proste sprawdzenie bepieczeństwa.|jak ograniczyć dostęp do formularza dodawania posta.|jak użyć metody po stronie serwera dla zwiększenia bezpieczeństwa.
paragraphs: 60
---
Widzieliśmy jak łatwo utworzyć nowy post z konsoli przeglądarki bezpośrednio używając funkcji bazy danych `Posts.insert`, ale nie możemy oczekiwać, że nasi użytkownicy będą otwierali konsolę przy każdym tworzeniu nowego posta.
W końcu będziemy musieli zbudować pewien interfejs użytkownika, aby pozwolić użytkownikom dodawać nowe posty z poziomu aplikacji.
### Budowanie strony Nowy Post
Zaczniemy od zdefiniowania ścieżki dla naszej nowej strony:
~~~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" %>
Użyjemy funkcji routera `data`, aby ustawić kontekst danych szablonu `postPage`. Pamiętaj, że cokolwiek wstawiliśmy do kontekstu danych, będzie dostępne jako `this` w obrębie helperów szablonu.
### Dodawanie linka do nagłówka.
Po zdefiniowaniu ścieżki, możemy dodać link do strony umożliwiającej dodanie posta w nagłówku:
~~~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" %>
Ustawienie ścieżki oznacza, że jeżeli użytkownik przejdzie do URL `/submit`, Meteor wyświetli szablon `postSubmit`. Napiszmy zatem ten szablon:
~~~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" %>
Uwaga: jest tu wiele znaczników pochodzących z Bootstrap Twittera. Podczas gdy tylko elementy formularza są istotne, dodatkowe znaczniki pomogą w upiększeniu naszej aplikacji. Powinna wyglądać teraz podobnie do:
<%= screenshot "7-1", "Formularz dodawania posta" %>
Jest to prosty formularz. Nie musimy się martwić o wykonanie konkretnej akcji, ponieważ będziemy przechwytywali zdarzenie `wyślij` na forumlarzu i uaktualniali dane za pomocą JavaScript. (Nie ma sensu dostarczać na wszelki wypadek rozwiązania nie-JavaScript, gdy weźmiesz pod uwagę, że aplikacja Meteora jest całkowicie niefunkcjonalna po wyłączeniu obsługi JavaScript).
### Tworzenie postów
Połączmy handler zdarzeń (ang. event handler) ze zdarzeniem `submit` formularza. Lepiej użyć zdarzenie `submit`, niż `click` na przycisku, jako że to pokryje wszystkie możliwe sposoby wysyłania postów (jak na przykład wciśnięcie przycisku Enter w polu URL).
~~~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", "Dodana strona do dodawania postów podlinkowana w nagłówku strony." %>
Powyższa funkcja używa [jQuery](http://jquery.com) do parsowania wartości różnych pól formularza i tworzenia nowego obiektu posta z wyników parsowania. Musimy się upewnić, że używamy `preventDefault` na parametrze `event` naszego handlera aby upewnić się, że przeglądarka nie będzie kontynuowała i próbowała wysłać formularza.
Ostatecznie, możemy przekierować uzytkownika to strony nowego posta. Funkcja kolekcji `insert` zwraca wygenerowany `id` dla obiektu, który został wstawiony do bazy danych, który użyje funkcja Routera `go()` aby utworzyć docelowy URL.
Końcowym rezultatem jest to, że po wciśnieciu przycisku `wyślij` przez użytkownika, tworzony jest post i użytkownik jest natychmiastowo zabierany do strony z dyskusją na temat tego nowego posta.
### Dodanie kilku środków bezpieczeństwa
Tworzenie postów ma się dobrze, ale nie chcemy na to pozwolić dowolnemu użytkownikowi: chcemy, aby się zalogowali przed dodaniem posta. Oczywiście możemy zacząć od ukrycia formularza wprowadzania nowego posta dla niezalogowanych użytkowników. Mimo to, użytkownik mógłby ukrycie utworzyć post w konsoli przeglądarki bez zalogowania się, a nie możemy sobie na to pozwolić.
Na szczęście bezpieczeństwo danych jest wbudowane w podstawy kolekcji Meteora: jest to po prostu domyślnie wyłączone, gdy tworzysz nowy projekt. Pozwala to na łatwy start i budowanie własnej aplikacji zostawiając nudne sprawy na sam koniec.
Nasza aplikacja już nie potrzebuje tej pomocy, zatem usuńmy pakiet `insecure`:
~~~bash
$ meteor remove insecure
~~~
<%= caption "Terminal" %>
Po wykonaniu tej komendy zauważyszm że formularz posta przestanie działać. Dzieje się to, ponieważ bez pakietu `insecure`wstawianie danych po stronie klienta _przestaje być dozwolone_. Musimy dodać Metorowi wyraźne zasady pozwalające na wstawianie postów przez klienta lub wstawiać posty po stronie serwera.
### Zezwolenie na wstawianie postów
Aby rozpocząć, pokażemy jak zezwolić na wstawianie nowych postów po stronie klienta, aby formularz zaczął znowu działać poprawnie. Jak się okaże później, wybierzemy jeszcze inny sposób, ale na teraz poniższy kod wystarczy, aby naprawić działanie aplikacji:
~~~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", "Usunieto insecure i zezwolono na zapisy do kolekcji posts." %>
Wołamy `Posts.allow`, które przekazuje Meteorowi "jest to zbiór okoliczności dla których klient ma zezwolenie różnych akcji na kolekcji `Posts`". W tym przypadku, mówimy "klient może wstawiać posty pod warunkiem posiadania `userId`".
`userId` użytkownika przeprowadzającego zmianę jest przekazywany do `allow` i `deny` (lub zwraca `null`, jeżeli żaden użytkownik nie jest zalogowany), co jest prawie zawsze pożyteczne. I skoro konta użytkowników są powiązane z głównym modułem Meteora możemy zawsze polegać na prawidłowości `userId`.
Zdołaliśmy upewnić się, że musisz być zalogowany, aby móc tworzyć posty. Spróbuj się wylogować i utworzyć posta. Powinieneś zobaczyć w konsoli przeglądarki jak poniżej:
<%= screenshot "7-2", "Insert failed: Access denied " %>
Jednakże, wciąż musimy dać sobie radę z kilkoma problemami:
- Wylogowani użytkownicy nadal mają dostę do formularza tworzenia posta
- Post nie jest związany z użytkownikiem w żaden sposób (i nie ma żadnego kodu po stronie serwera, który by to obsługiwał).
- Można utworzyć wiele postów, które będą wskazywały na ten sam URL.
Naprawmy te problemy.
### Zabezpieczanie dostępu do formularza nowego posta
Zacznijmy od zabezpieczenia przed zobaczeniem formularza dla niezalogowanych użytkowników. Zrobimy to na poziomie routera przez zdefiniowanie funkcji *podłączonej do ścieżki* (ang. hook).
Funkcja taka przechwyca proces przekierowania i potencjalnie może zmienić akcję, którą podejmuje router. Możesz myśleć o tym jako o strażniku, który sprawdza twoje dane przez wpuszczeniem Ciebie do środka (lub odmowie dostępu).
Co potrzebujemy, to sprawdzenie czy użytkownik jest zalogowany, a jeżeli nie jest, wyrenderowanie szablonu `accessDenied` zamiast oczekiwanego szablonu `postSubmit` (następnie zatrzymujemy wykonywanie kolejnych funkcji przez router). Zmieńmy zatem router.js tak, jak poniżej:
~~~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" %>
Tworzymy również szablon dla strony z odmową dostępu:
~~~html
<template name="accessDenied">
<div class="alert alert-error">Nie masz tu dostępu! Proszę się zalogować.</div>
</template>
~~~
<%= caption "client/views/includes/access_denied.html" %>
<%= commit "7-3", "Odmowa dostępu do strony nowych postów dla niezalogowanych użtytkowników." %>
Jeżeli skierujesz się teraz do http://localhost:3000/submit/ nie będąc zalogowanym, powinieneś zobaczyć:
<%= screenshot "7-3", "Szablon odmowy dostępu" %>
Pozytywnym aspektem funkcji podpiętych pod router jest to, że są _reaktywne_. Oznacza to, że możemy działać deklaratywnie i nie musimy martwić się o callbacki czy cokolwiek podobnego, gdy użytkownik jest zalogowany. Gdy status zalogowania się użytkownika ulega zmianie, szablony strony routera natychmiastowo zmienia się z `accessDenied` na `postSubmit` bez konieczności pisania dodatkowego kodu.
Zaloguj się, następnie spróbuj odświeżyć stronę. Możesz czasami zobaczyć krótkie mignięcie szablonu odmowy dostępu przed pojawieniem się strony pozwalającej na dodanie posta. Przyczyna tego jest taka, że Meteor rozpoczyna renderowania tak szybko, jak to jest tylko możliwe, zanim skontaktuje się z serwerem i sprawdzi, czy bieżący użytkownik (zapisany w lokalnej bazie danych) nawet istnieje.
Aby uniknąć tego problemu (który jest częstą klasą problemów, z którymi będziesz musiał się zmierzyć poznając subtelności związanych z opóźnieniem przy komunikacji klient-serwer), wyświetlimy po prostu na ekranie szablon ładowania na krótką chwilę podczas sprawdzania, czy użytkownik ma dostęp.
W końcu w tym momencie nie wiemy czy użytkownik ma prawidłowe dane do zalogowania i nie możemy pokazać ani `accessDenied` ani `postSubmit` aż tego nie sprawdzimy.
Modyfikujemy zatem naszą funkcję podpiętą do routera aby używała szablonu do ładowania podczas gdy `Meteor.logginIn()` zwraca true:
~~~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." %>
### Ukrywanie linka
Najłatwiejszym sposobem aby zapobiec przypadkowy dostęp użytkownikom do tej strony jest ukryć link. Osiągniemy to całkiem łatwo przez:
~~~html
<ul class="nav">
{{#if currentUser}}<li><a href="{{pathFor 'postSubmit'}}">Dodaj post</a></li>{{/if}}
</ul>
~~~
<%= caption "client/views/includes/header.html" %>
<%= commit "7-5", "Pokazuj link Dodaj post jeżeli uzytkownik jest zalogowany." %>
Helper `currentUser` jest dostarczony przez pakiet `accounts` i jest odpowiednikiem `Meteor.user()` dostarczanego przez Handlebars. Ponieważ jest reaktywny, link pojawi się lub nie w zależności od tego, czy się wylogujesz, czy zalogujesz do aplikacji.
### Serwerowe metody Meteora: Większy stopień bezpieczeństwa i abstrakcji
Zdołaliśmy zabezpieczyć dostęp do strony nowych postów dla niezalogowanych użytkowników, i odmówić takim użytkownim możliwość dodawania postów nawet jeżeli oszukują i używają konsoli przeglądarki. Jest jednak jeszcze kilka rzeczy, o które musimy się zatroszczyć:
- Dodanie znacznika czasu do posta.
- Upewnienie się, że ten sam URL nie można dodać więcej, niż jeden raz.
- Dodanie szczegółów o autorze posta (ID, nazwa użytkownika, itd.).
Możesz sobie pomyśleć, że wszystko to możemy osiągnąć w funkcji obsługującej zdarzenia `submit`. Gdybyśmy to zrobili, napotkalibyśmy nową grupę problemów:
- Jeżeli chodzi o znacznik czasu, musielibyśmy polegać na prawidłowo ustawionym czasie po stronie klienta, co nie zawsze nastąpi
- Klient nie będzie wiedział o _wszystkich_ URL, które zostały opublikowane na stronie. Będą jedynie wiedzieli o postach, które bieżąco widzą (później dokładnie opiszemy jak to działa), więc nie ma pewnego sposobu na wymuszenie unikalnych adresów URL po stronie klienta.
- Ostatecznie, chociaż teoretycznie _możemy_ dodawać szczegóły użytkownika po stronie klienta, nie wymuszalibyśmy ich dokładności i zgodności z prawdą i prowadziłoby to do nadużyć związanych z używaniem konsoli przeglądarki.
Z powodów wymienionych powyżej lepiej jest utrzymywać proste funkcje obsługujące zdarzenia i jeżeli robimy coś więcej niż tylko podstawowa wstawianie lub modyfikacje kolekcji, używać **Metod** Meteora (metod przez duże 'M').
Metoda Meteora jest funkcją serwerową wykonywaną po stronie serwera, ale wywoływaną po stronie klienta. Nie jest to dla nas całkowicie obce -- w szczególności funkcje kolekcji `insert`, `update` i `remove` są wszystkie Metodami. Sprawdźmy jak zaimplementować własną.
Wróćmy do `post_submit.js`. Zamiast wstawiania bezpośrednio do kolekcji `Posts`, zawołamy Metodę o nazwie `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" %>
Funkcja `Meteor.call` woła Metodę o nazwie określonej w pierwszym parametrze funkcji. Możesz dodać parametry do wywołania funkcji (w tym przypadku obiekt `post` skonstruowany na podstawie formularza) i ostatecznie dodać callback, który zostanie uruchomiony po zakończeniu Metody. Tutaj po prostu ostrzegamy użytkownika czy wystąpił jakiś problem lub przekierowujemy go nowoutworzonej strony dyskusji konkretnego posta.
Następnie zdefiniujemy Metodą w pliku `collections/posts.js`. Usuniemy blok `allow()` z `posts.js`, ponieważ Metoda Meteora i tak go ominie. Pamiętaj, że Metody są wykonywane po stronie serwera, więc Meteor uważa, że można im ufać.
~~~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", "Uzycie Metody przy dodawaniu posta." %>
Metoda ta jest trochę skomplikowana, ale mamy nadzieje że możesz nadążyć.
Na początkek definiujemy zmienną `user` i sprawdzamy, czy post z tym samym linkiem już nie istnieje. Następnie sprawdzamy, czy użytkownik jest zalogowany, rzucając błąd (który końcowo będzie wyświetlony w przeglądarce), jeżeli nie jest. Przeprowadzamy także prostą walidację obiektu posta, aby upewnić się, że post zawiera tytuł.
Następnie, jeżeli istnieje już post z tym samym URL, rzucamy błąd `302` (który oznacza przekierowanie), przekazaując użytkownikowi, że powinien sprawdzić poprzednio tworzony post.
Klasa `Error` Meteora ma trzy parametry. Pierwszy, (`error`) będzie numerycznym kodem `302`, drugi (`reason`) zawiera wyjaśnienie zrozumiałe dla człowieka i trzeci (`details`) może być jakąkolwiek użyteczną informacją.
W naszym przypadku, użyjemy trzeci argument aby przekazać ID posta, który właśnie znaleźliśmy. Uwaga: użyjemy tego później aby przekierować użytkownika do jeszcze nieistniejącego posta.
Jeżeli wszystkie kroki są sprawdzone i przechodzą poprawnie, bierzemy pola, które chcemy wstawić (upewniając się, że użytkownik wołający tą Metodę w konsoli przeglądarki nie może dodać fałszywych danych do bazy) i dodajemy pewne informacje o uzytkowniku dodającym posta -- jak również bieżący czas -- do posta.
Ostatecznie, wstawiamy post do bazy danych i zwracamy nowy `id` użytkownikowi.
### Sortowanie postów
Teraz skoro mamy dostęp do czasu wstawienia wszystkich postów, ma sens upewnienie się, że są posortowane ze względu na ten atrybut. Aby to uczynić, możemy użyć operatora Mongo `sort`, który oczekuje obiektu składającego się z kluczy po których posortować i znaku oznaczającego czy sortowanie ma być wykonane rosnąco czy malejąco.
~~~js
Template.postsList.helpers({
posts: function() {
return Posts.find({}, {sort: {submitted: -1}});
}
});
~~~
<%= caption "client/views/posts/posts_list.js" %>
<%= highlight "3" %>
<%= commit "7-7", "Sortowanie postów po znaczniku czasu." %>
Zabrało to trochę pracy, ale wreszcie mamy interfejs użytkownika, który pozwala bezpiecznie wstawiać nowe dane do naszej aplikacji!
Jakakolwiek aplikacja, która pozwala użytkownikom na tworzenie nowych treści musi dać im także sposób na ich późniejszą edycję lub usuwania. Poświęcony jest temu rozdział Edytowanie Postów.