-
Notifications
You must be signed in to change notification settings - Fork 12
/
07-creating-posts.md.erb
423 lines (307 loc) · 20 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: Beiträge erstellen
slug: creating-posts
date: 0007/01/01
number: 7
contents: Lerne wie man Beitragsdaten client-seitig versendet.|Implementiere einen Sicherheitscheck.|Begrenze den Zugriff zum Formular.|Lerne wie man eine serverseitige Methode für zusätzliche Sicherheit nutzt.
paragraphs: 60
---
Wir haben nun gesehen wie einfach es ist, Beiträge über die Konsole mit dem Datenbankaufruf `Posts.insert` anzulegen. Aber wir können natürlich nicht erwarten, dass unsere Benutzer die Konsole benutzen.
Also brauchen wir ein Benutzer-Interface, um unseren Benutzern die Möglichkeit zu geben, neue Beiträge anzulegen.
### Aufbau der Seite zum Anlegen eines Beitrages
Wir fangen an, indem wir eine neue Route für eine Seite definieren:
~~~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" %>
Wir benutzen die Funktion `data` des Routers, um den Kontext `postPage` des Templates zu setzen. Zur Erinnerung: Was immer wir in den Kontext legen, wird als `this` innerhalb des Template-Helpers verfügbar sein.
### Hinzufügen eines Links zur Überschrift
Da die Route nun angelegt ist, können wir nun einen Link darauf in der Navigation unserer Seite hinzufügen:
~~~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" %>
Das Anlegen der Route bedeutet, dass wenn der Benutzer die URL `/submit` aufruft, Meteor das Template `postsubmit` anzeigen wird. Wir legen dieses Template nun an:
~~~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" %>
Bemerkung: Das ist einiges an Markup. Das wird durch die Benutzung von Twitters Bootstrap verursacht. Eigentlich sind nur die Formularelemente wichtig. Das restliche Markup hilft aber, unser App ein wenig besser aussehen zu lassen. Das gerenderte Template sollte nun wie folgt aussehen:
<%= screenshot "7-1", "Das Formular zum Anlegen von Beiträgen" %>
Das ist ein einfaches Formular. Wir brauchen uns über die ´Action´ des Formulars keine Gedanken zu machen. Den Submit-Event des Formulars werden wir ohnehin abfangen und die Daten per JavaScript aktualisieren. (Es macht auch keinen Sinn einen Fallback für nicht vorhandenes JavaScript im Browser einzurichten, da eine Meteor-App ohne JavaScript nicht funktioniert.)
### Das Anlegen von Beiträgen
Wir verbinden nun das Formular mit einem Event-Handler. Am besten verwenden wir dafür den `submit`-Event (anstatt z.B. den Click-Event des Buttons). Das hat den Vorteil, dass alle möglichen Wege für das Absenden des Formulars abgedeckt werden (wie zB. das Benutzen der Return-Taste im URL-Feld).
~~~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." %>
Diese Funktion verwendet [jQuery](http://jquery.com), um die Werte aus den verschiedenen Formularfeldern zu entnehmen und ein neues Beitrags-Objekt anzulegen. Wir müssen dabei beachten, dass die Methode `preventDefault()` auf dem Parameter `event` in unserem Handler aufgerufen wird. Ansonsten würde der Browser versuchen, dass Formular abzusenden.
Am Ende können wir per Router auf die Seite des neuen Beitrag umleiten. Die Funktion `insert()` einer Collection gibt die erzeugte `id` des Objektes zurück, welche in die Datenbank eingefügt wurde. Die Funktion `go()` des Routers erzeugt uns die URL aus ihren Parametern.
Das Resultat ist, dass wenn der Benutzer auf den Absende-Button drückt, ein Beitrag angelegt wird und der Benutzer auf die Diskussionsseite für diesen Beitrag gelangt.
### Etwas mehr Sicherheit
Das Anlegen von Beiträgen funktioniert soweit. Aber wir wollen nicht, dass jeder x-beliebige Besucher der Webseite Beiträge anlegen kann. Wir möchten, dass Benutzer dafür eingeloggt sein müssen. Sicherlich könnten wir das Formular zum Anlegen von neuen Beiträgen vor ausgeloggten Benutzern verstecken. Aber ein findiger Benutzer könnte die Beiträge immer noch auf der Konsole anlegen. Das wollen wir natürlich nicht.
Datensicherheit ist glücklicherweise direkt in den Meteor Collections eingebaut. Sie ist nur standardmässig deaktiviert, wenn ein neues Projekt angelegt wurde. Das erlaubt es einfach mit dem Aufbau der App anzufangen und den langweiligen Teil später zu erledigen.
Diese Stützräder braucht unsere App jetzt nicht mehr. Wir nehmen sie nun weg. Wir entfernen das Package `insecure`:
~~~bash
$ meteor remove insecure
~~~
<%= caption "Terminal" %>
Danach wirst du feststellen, das das Formular nicht mehr funktioniert. Ohne das Package `insecure` sind client-seitige Einfügeoperationen in Collections _nicht mehr erlaubt_. Wir müssen entweder explizite Regeln anlegen, um Meteor mitzuteilen, dass es OK ist auf dem Client Objekte anzulegen oder wir müssen das Einfügen auf die Server-Seite verlegen.
### Das Einfügen von Beiträgen wieder erlauben
Wir zeigen nun, wie man das client-seitige Anlegen von Beiträgen erlaubt. Damit wird das Formular wieder funktionsfähig. Später werden wir zwar eine andere Technik benutzen, aber im Moment ist der folgende Code der einfachere Weg:
~~~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." %>
Wir rufen die Methode `allow()` auf der Posts-Collection auf. Diese teilt Meteor mit unter welchen Umständen Clients einen Beitrag einfügen dürfen. In diesem Fall sagen wir "Clients dürfen neue Beiträge anlegen, wenn sie eine `userId` besitzen".
Die `userId` des Benutzers, der das Anlegen des Beitrags vornimmt, wird an die Aufrufe von `allow` und `deny` weitergeleitet (oder liefert `null`, wenn kein Benutzer eingeloggt ist). Da die Benutzer-Accounts im Kern von Meteor verankert sind, können wir uns darauf verlassen, dass die `userId` immer korrekt gesetzt ist.
Wir haben also sichergestellt, dass der Benutzer immer eingeloggt sein muss, um einen Beitrag anzulegen. Versuch einmal, dich auszuloggen und einen Beitrag anzulegen. Du solltest Folgendes in der Konsole sehen:
<%= screenshot "7-2", "Insert failed: Access denied " %>
Aber wir sind noch nicht fertig:
- Ausgeloggte Benutzer können immer noch das Anlegeformular sehen.
- Der Beitrag ist noch nicht mit dem Benutzer verknüpft (und es gibt keinen Code auf dem Server um dies sicherzustellen)
- Mehrere Beiträge können angelegt werden, die die selbe URL enthalten.
Versuchen wir diese Probleme anzugehen.
### Absichern des Zugangs zum Anlegeformular eines Beitrages
Wir fangen an, indem wir ausgeloggte Benutzer daran hindern, das Anlegeformular aufzurufen. Wir werden dies im Router implementieren, in dem wir einen *route hook* definieren.
Ein Hook fängt den Routing-Prozess ab und kann die Aktionen, die ein Router vornimmt verändern. Du kannst es als eine Art Wachmann betrachten, der deinen Ausweis überprüft, bevor er Dich hereinlässt (oder abweist).
Dazu müssen wir überprüfen, ob ein Benutzer eingeloggt ist. Wenn nicht muss anstelle des Templates `postSubmit` das Template `accessDenied` ausgegeben werden. Der Router wird dazu wie folgt angepasst:
~~~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" %>
Wir legen jetzt ein Template für den verweigerten Zugriff an:
~~~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." %>
Wenn du nun http://localhost:3000/submit/ aufrufst ohne eingeloggt zu sein, solltest du Folgendes sehen:
<%= screenshot "7-3", "Das Template für den verweigerten Zugriff" %>
Was Routing-Hooks richtig nett macht ist, dass sie _reaktiv_ sind. Das bedeutet, wir können deklarativ arbeiten und müssen uns nicht um Callbacks oder ähnliches kümmern, wenn der Benutzer sich einloggt. Wenn sich der Login-Zustand eines Benutzers ändert, wird das Seitentemplate sofort von `accessDenied` zu `postSubmit` geändert - ohne das wir explizit Code dafür schreiben müssen.
Log dich ein und versuch dann die Seite neu zu laden. Du wirst eventuell das Template "Zugriff verweigert" für einen kurzen Moment aufflackern sehen, bevor das Anlageformular erscheint. Der Grund dafür ist, dass Meteor Templates so früh wie möglich rendert. Es kann sein, dass dies früher geschieht als der Server mitteilen kann, ob der Benutzer eingeloggt ist. (Der Zustand wird übrigens im LocalStorage des Browsers zwischengespeichert.)
Um dieses Problem zu verhindern (es handelt sich um ein Problem, welches Du häufiger sehen wirst, wenn Du Dich mit den Eigenheiten von Latenz zwischen Server und Client beschäftigst), werden wir für eine kurze Zeit einen Ladebildschirm anzeigen und warten bis feststeht, ob der Benutzer eingeloggt ist oder nicht.
Dies ist notwendig, da wir ohne den Server nicht entscheiden können, ob der Benutzer eine korrekte Authorisierung vorgenommen hat oder nicht. Solange können wir weder das Template `accessDenied` noch das Template `postSubmit` anzeigen.
Wir ändern also unseren Hook, um unser Lade-Template anzuzeigen. Dies bleibt solange `Meteor.loggingIn()` den Wert `true` zurückliefert:
~~~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." %>
### Links verstecken
Die einfachste Möglichkeit ausgeloggte Benutzer daran zu hindern, versehentlich das Anlegeformular zu erreichen, ist es den Link zu verstecken. Das geht ziemlich einfach:
~~~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." %>
Der Helper `currentUser` wird durch das Package `accounts` und sein Handlebar-Äquivalent `Meteor.user()` zur Verfügung gestellt. Weil der Helper ebenfalls reaktiv ist, wird der Link angezeigt, sobald Du Dich einloggst. Wenn Du Dich ausloggst verschwindet er automatisch.
### Die Meteor-Methode: Bessere Abstraktion und Sicherheit
Wir haben es geschafft, den Zugriff auf das Anlegeformular für ausgeloggte Benutzer zu beschränken. Auch ist es jetzt für sie nicht mehr möglich, auf der Konsole Beiträge anzulegen. Aber ein paar Dinge bleiben noch übrig, um die wir uns kümmern müssen:
- Zeitstempel für die Beiträge
- Verhindern von zwei Beiträgen, die die selbe URL benutzen
- Details über den Author des Beitrags (id, Benutzername, usw.)
Du wirst dir vielleicht denken, dass wir dass alles im Event-Handler `submit` erledigen können. Realistisch betrachtet, würde das allerdings zu einer Menge Probleme führen.
- Für den Timestamp müssten wir uns darauf verlassen, dass der Computer des Benutzers die korrekte Uhrzeit hat. Das ist leider nicht immer der Fall.
- Clients kennen nur einen Teil der benutzten URLS. Nämlich die, deren Beiträge sie sehen können (wir werden uns nachher anschauen, wie das genau funktioniert). Also gibt es keinen zuverlässigen Weg, client-seitig die Eindeutigkeit der URL zu überprüfen.
- Schließlich, obwohl wir die Benutzerdaten client-seitig hinzufügen _könnten_, können wir nicht gewährleisten, dass alle Angaben stimmen. Das könnte von findigen Personen in der Browser-Konsole missbraucht werden.
Aus all diesen Gründen ist es besser, wenn wir unsere Event-Handler einfach halten - und wenn wir mehr als einfachste Einfüge- oder Update-Operationen benötigen, benutzen wir eine **Methode**
Eine Meteor-Methode ist eine serverseitige Funktion, die vom Client aufgerufen wird. Genau genommen kennen wir sie schon -- hinter den Kulissen sind die `insert`, `update` und `remove`-Funktionen der `Collection` allesamt Methoden. Schauen wir uns mal an, wie wir selber welche erzeugen können.
Lass uns dazu noch mal die Datei `post_submit.js` anschauen. Anstelle direkt in die Collection `Posts` einzufügen, rufen wir nun eine Methode namens `post` auf:
~~~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" %>
Die Funktion `Meteor.call` ruft eine Methode auf, die durch ihren ersten Parameter spezifiziert wird. Du kannst weitere Parameter (in diesem Fall das Objekt `post`, welches wir aus dem Formular zusammengebaut haben) und einen Callback übergeben. Der Callback wird ausgeführt, wenn die Methode auf dem Server abgearbeitet ist. In diesem Fall geben wir Rückmeldung an den Benutzer, ob Probleme aufgetreten sind oder leiten ihn auf die Diskussionsseite für den Beitrag weiter.
Danach definieren wir die neue Methode in der Datei `collections/posts.js`. Wir entfernen den Block `allow()`, weil Meteor-Methoden diesen ohnehin umgehen. Du erinnerst dich vielleicht: Methoden werden auf dem Server ausgeführt. Meteor nimmt an, dass diese deshalb vertrauenswürdig sind.
~~~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." %>
Diese Methode ist ein wenig komplizierter, aber wir hoffen du kannst uns folgen.
Zuerst definieren wir die Variable `user`. Wir überprüfen, ob ein Beitrag mit dem selben Link schon existiert. Dann wird geschaut, ob der Benutzer eingeloggt ist. Ein Fehler wird geworfen, wenn das nicht der Fall ist (der Fehler kann später im Browser angezeigt werden). Wir validieren danach den Beitrag auf einfache Weise, um sicher zu gehen, dass der Beitrag einen Titel hat.
Als Nächstes, falls ein weiterer Beitrag mit der selben URL existiert, werfen wir einen Fehler `302` (der einen Redirect entspricht). Dadurch können wir dem Benutzer mitteilen, dass er sich den vorherigen Beitrag anschauen soll.
Meteors Klasse `Error` nimmt drei Parameter auf. Der erste (`error`) ist in diesem Fall der numerische Code `302`. Der Zweite (`reason`) ist eine kurze menschenlesbare Fassung des Fehlers. Der dritte (`details`) kann dazu genutzt werden hilfreiche zusätzliche Information weiterzugeben.
In unserem Fall, benutzen wir den dritten Parameter, um die ID des bereits existierenden Beitrags weiterzureichen. Spoiler: Wir werden dies später benutzen, um den Benutzer auf die Seite des vorherigen Beitrags weiterzuleiten.
Wenn all diese Überprüfungen erfolgreich waren, übernehmen wir lediglich die Felder des Objektes, die wir einfügen wollen (um zu vermeiden, dass der Benutzer weitere Felder in unsere Datenbank einfügen kann, z.B. in dem er die Konsole verwendet). Ausserdem fügen wir zusätzliche Information über den Benutzer, sowie den Zeitpunkt des Anlegens in den Beitrag ein.
Als Letztes fügen wir den Beitrag ein und geben die ID des erzeugten Objekts zurück.
### Sortieren von Beiträgen
Jetzt, da alle Beiträge über ein Datum verfügen, macht es Sinn danach zu sortieren. Um das zu erreichen, können wir Mongos Operator `sort` verwenden. Dieser erwartet, dass ein Objekt aus Schlüsseln besteht, nach deren Werten sortiert werden kann. Zusätzlich gibt ein Vorzeichen an, ob wir aufsteigend oder absteigend sortieren.
~~~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." %>
Es hat ein wenig Arbeit erfordert, aber wir haben nun ein Benutzerinterface, welches es unseren Benutzer erlaubt, auf abgesicherte Art und Weise Daten in unsere App einzugeben.
Aber jede App, die den Benutzer Inhalte anlegen lässt, muss auch einen Weg anbieten diese zu ändern und zu löschen. Das werden wir im Kapitel "Ändern von Beiträgen" behandeln.