Nutzerverwaltung und Material¶
Wir haben bis jetzt immer Bootstrap als CSS-Framework verwendet. Nun wollen wir stattdessen mal Angular Material nutzen. Es sei erwähnt, dass es sogar ein Framework gibt, dass beides vereint, nämlich Material Design for Bottstrap (MDB). Das ist aber nur teilweise kostenlos.
Inhaltlich geht es uns nicht nur um die Verwendung von Angular Material, sondern um eine Nutzer- und Rollenverwaltung, um einige weitere Konzepte anzuwenden, wie z.B. rollenbasierter Zugriff auf Komponenten und die Verschlüsselung und das Prüfen von Passwörtern.
Video aus der Vorlesung am 10.1.2022
Angular-Anwendung mit Registrierung und Login¶
Wir erstellen uns dafür eine Angular-Anwendung users
mit Routing mit einer Navigations-, einer Registrierungs- und eine Login-Komponente.
ng new users --routing
Wir fügen dieser Anwendung [Angular Material] hinzu:
cd users
ng add @angular/material
Sie werden gefragt, welches prebuilt theme
Sie verwenden wollen. Nehmen Sie ruhig Custom
:
ℹ Using package manager: npm
✔ Found compatible package version: @angular/material@12.2.13.
✔ Package information loaded.
The package @angular/material@12.2.13 will be installed and executed.
Would you like to proceed? Yes
✔ Package successfully installed.
? Choose a prebuilt theme name, or "custom" for a custom theme:
Indigo/Pink [ Preview: https://material.angular.io?theme=indigo-pink ]
Deep Purple/Amber [ Preview: https://material.angular.io?theme=deeppurple-amber ]
Pink/Blue Grey [ Preview: https://material.angular.io?theme=pink-bluegrey ]
Purple/Green [ Preview: https://material.angular.io?theme=purple-green ]
❯ Custom
und beantworten Sie ruhig alle weiteren Fragen mit y
(es):
✔ Package successfully installed.
? Choose a prebuilt theme name, or "custom" for a custom theme: Custom
? Set up global Angular Material typography styles? Yes
? Set up browser animations for Angular Material? Yes
UPDATE package.json (1135 bytes)
✔ Packages installed successfully.
CREATE src/custom-theme.scss (1416 bytes)
UPDATE src/app/app.module.ts (502 bytes)
UPDATE angular.json (3072 bytes)
UPDATE src/index.html (573 bytes)
UPDATE src/styles.css (181 bytes)
Die Verwendung von Angular Material ist leider etwas aufwendiger, als die von Bootstrap, weil alle Komponenten, die Sie von Angular Material verwenden wollen, explizit in der app.module.ts
importiert werden müssen. Wir schauen uns das an einigen Beispielen an.
Navigation schematic - nav¶
Allerdings hat Angular Material ein recht nützlich Feature, nämlich sogenannte Schematics. Das sind vorgefertigte Designkomponenten, die man häufig verwendet. Ein erstes Beispiel für ein solches Schematic ist das Navigation schematic mit dem wir unsere Navigationskomponente nav
erstellen:
ng generate @angular/material:navigation nav
Die allgemeine Syntax zur Erstellung einer Navigationskomponente ist ng generate @angular/material:navigation <component-name>
. Wir erzeugen somit also die Komponente namens nav
.
Adress form schematic - register, login¶
Das Adress form schematic erzeugt zwar ein Formular für die Eingabe einer Adresse, aber da wir für unser Registrierungs-Formular und auch für das Login-Formular viele CSS-Klassen aus dem Material-Formular verwenden wollen, erzeugen wir uns mithilfe des Adress form schematic auch die register
- und die login
-Komponente:
ng generate @angular/material:address-form register
ng generate @angular/material:address-form login
Dashboard schematic - home¶
Zuletzt verwenden wir noch das Dashboard schematic, um uns eine home
-Komponente zu erzeugen:
ng generate @angular/material:dashboard home
Routing und AppComponent¶
Alle drei Komponenten rufen wir zunächst per Routing auf und passen deshalb unsere app-routing.module
wie folgt an:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
In der app.component.html
binden wir nur die nav
-Komponente ein (nicht <router-outlet></router-outlet>
- das kommt in die nav
-Komponente):
1 |
|
NavComponent¶
Nun passen wir unsere NavComponent
an und öffnen dazu die nav.component.html
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Wichtig ist, dass die <router-outlet></router-outlet>
an die Stelle des Kommentars <!-- Add Content Here -->
kommt (Zeile 18
). Genau dort wird der Inhalt eingebunden, d.h. die Komponenten, die per Routing aufgerufen und eingebunden werden (bei uns home
, register
oder login
). Der Eintrag in Zeile 15
ist wie eine Überschrift und in den Zeilen 5-8
werden die Menüeinträge (und die dazugehörigen Routen) definiert. Es entsteht folgende (für die home
-Komponente):
Verrringern Sie auch die Breite des Viewports, um zu sehen, wie responsive die einzelnen Komponenten (inkl. nav
-Komponente) bereits sind.
Register-Formular¶
Wir passen nun das Formular zur Registrierung an. Das Standard-Shipping-Adress-Formular zeigt, was mit Formularen im Material-Design alles möglich ist. Wir passen das Formular an, um die Registrierung einer Nutzerin mit ihrer Rolle zu ermöglichen:
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 |
|
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 |
|
1 2 3 4 5 6 7 |
|
Es entsteht folgendes Formular:
Backend¶
Wir erweiteren das Backend, das wir für die Verwaltung von Member
-Einträgen in die MongoDB erstellt hatten. Wir führen entsprechende Erweiterungen durch. Der Verzeichnisbaum im Projekt sieht nun so aus:
Es wurde also ein models
-Verzeichnis erstellt, in dem sowohl die members.js
(also das Model für die Members
) als auch das neue users.js
enthalten ist. Ebenso wurden die Dateien members.js
und users.js
in dem Verzeichnis routes
erstellt, in dem die jeweiligen Routen für die Users
und die Members
definiert sind.
Die server.js
sieht nun so aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Die db.js
hat folgendes Aussehen:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
und im .env
-File ist nur
DB_CONNECTION = mongodb://127.0.0.1:27017/members
hinterlegt. Die in der server.js
definierte Route /initdb
bewirkt den Aufruf von initdb.js
, die die members
-Collection befüllt. Das kann aber auch weggelassen werden - oder Sie erweitern das Skript noch um eigene Einträge für die users
-Collection:
initdb.js
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 |
|
Die beiden models
sind wie folgt definiert:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
1 2 3 4 5 6 7 8 9 |
|
An dem routes/members.js
haben wir nichts geändert:
routes/members.js
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 |
|
Das Skript routes/users.js
ist prinzipiell gleich aufgebaut, wie das members.js
-Skript, denn auch dort sollen die CRUD
-Funktionen abgebildet werden, allerdings kommt noch die Verschlüsselung von Passwörtern hinzu.
Passwort-Verschlüsselung¶
Die Passwörter sollen nur verschlüsselt in der Datenbank abgelegt werden. Dazu verwenden wir das Paket bcrypt.
npm install bcrypt
Beim Einfügen eines neuen User
-Eintrags wird das übermittelte Passwort verschlüsselt, bevor es in die Datenbank gespeichert wird:
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 |
|
Wird ein neuer Eintrag in die Datenbank geschrieben (ab Zeile 13
), wird das Passwort mithilfe der hash
-Funktion von bcrypt
verschlüsselt. Dieser Passwort-Hash (siehe Zeilen 19
und 24
) werden gespeichert.
Von der GET
-Anfrage für einen user
gibt es zwei Routen. Die Route router.get('/:email',...)
gibt den Eintrag einer user
in für eine übergebene :email
zurück. Dieser Eintrag enthält das Passwort als hash
-Wert. Die Route router.post('/login/:email', ...)
erwartet im body
noch ein Passwort. Deshalb wird hier auch die POST
-Anfragemethode verwendet. Es wird überprüft, ob das übergebene Passwort dem gespeicherten Passwort entspricht. Dies geschieht mithilfe der compare
-Funktion von bcrypt
(siehe Zeile 41
).
Angenommen, es wird folgender Datensatz gepostet:
{
"firstname": "test1",
"lastname": "test1",
"email": "test1@htw-berlin.de",
"password": "test1",
"role": "User"
}
Dann wird ein Datensatz in die Datenbank gespeichert, der das gehashte Passwort enthält (bei Ihnen wird das Passwort und die _id
jeweils anders sein):
Die Anfrage GET http://localhost:3000/users/test1@htw-berlin.de
liefert entsprechend das Objekt zurück:
{
"_id": "61d2be50e101156611d31025",
"firstname": "test1",
"lastname": "test1",
"email": "test1@htw-berlin.de",
"password": "$2b$10$WoinjWBRiTmYe2lZED260euref2ieSPYhScHBUs./kpZZKXm06p.m",
"role": "User",
"__v": 0
}
Bei der Anfrage POST http://localhost:3000/users/login/test1@htw-berlin.de
muss im Body
des Request
auch noch ein Passwort übergeben werden:
Nur, wenn das Passwort korrekt ist, wird das Objekt gesendet, ansonsten wird ein Fehlerobjekt versendet:
Backend-Service im Frontend¶
Zur Anbindung an die oben beschriebene REST-API erstellen wir im Frontend einen Service
ng g s shared/backend
Diesen Service implementieren wir, wie hier beschrieben. Das bedeutet, dass wir zunächst das HttpClientModule
in der app.module.ts
importieren:
app.module.ts
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 |
|
Im BackendService
implementieren wir zunächst zwei Funktionen:
registerNewUser(user: User)
legt einen neuenUser
in der Datenbank an, d.h. der EndpunktPOST http://localhost:3000/users
wird aufgerufen und die neuenUser
-Daten imBody
des Requests übergeben,checkIfExist(email: string)
prüft, ob dieemail
bereits registriert ist, d.h. der EndpunktPOST http://localhost:3000/users/:email
wird aufgerufen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Prüfen, ob E-Mail bereits registriert¶
Wird eine neue E-Mail eingegeben, dann soll zunächst geprüft werden, ob diese bereits registriert ist. Dazu wird nicht erst das gesamte Formular submitted, sondern wir behandeln für das Eingabefeld der E-Mail direkt das change
-Ereignis. Das change
-Ereignis wird ausgelöst, nachdem die Eingabe im Eingabefeld abgeschlossen ist (d.h. entweder die Enter
-taste gedrückt wird oder der Fokus vom Eingaefeld auf ein anderes Eingabefeld wechselt). Soll jede Tastatureingabe behandelt werden, bietet sich dafür das input
-Ereignis an. Wir fügen hier die Behandlung des change
-Ereignisses hinzu:
25 26 27 28 29 30 31 32 33 |
|
Neu ist darin also der Eintrag (change)="checkIfExists($event.target)"
. Mithilfe der Funktion checkIfExists()
soll also geprüft werden, ob die E-Mail-Adresse bereits in der Datenbank gespeichert ist. Der Funktion wird $event.target
übergeben. Das ist das auslösende input
-Element.
Die checkIfExists()
-Funktion wird in der register.component.ts
definiert:
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
|
Die email
selbst wird als value
aus dem input
-Element ausgelesen (Zeile 77
). Die Konsoleausgaben können natürlich auch weg. In Zeile 80
wird die checkIfExist()
-Funktion des BackendService
aufgerufen. Dieser muss per dependency injection eingebunden werden. Wenn als response
ein Objekt zurückgesendet wird, dann existiert die E-Mail-Adresse bereits. Dann soll ein modaler Dialog geöffnet werden, der dies anzeigt. Diese openDialog()
-Funktion soll im Folgenden erstellt werden.
Dialoge mit Material¶
Angular Material unterstützt die Erstellung von Dialogen. Um die Material-Dialoge verwenden zu können, müssen wir diese in der app.module.ts
(darin auch schon die ExistDialogComponent
, die wir gleich erstellen):
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 |
|
Wir erstellen uns einen einfachen Dialog für die Nachricht, dass die E-Mail-Adresse bereits existiert. Dazu erstellen wir eine weitere einfache Komponente, die wir im register
-Ordner erstellen:
ng g c register/exist-dialog
Die exist-dialog.component.ts
können wir kleinstmöglich gestalten:
1 2 3 4 5 6 7 8 |
|
Für die exist-dialog.component.html
bedienen wir uns aus den Material-Dialog-Beispielen :
1 2 3 4 5 6 7 8 9 |
|
Nun können wir in der register.component.ts
die openDialog()
-Funktion implementieren. Wir zeigen die vollständige Datei (inkl. onSubmit()
):
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 |
|
Angenommen, wir geben nun eine E-Mail-Adresse in das Registrierungs-Formular ein, die bereits existiert, dann erscheint eine Nachricht:
Registrierung neue Nutzerin¶
Für die Registrierung einer neuen Nutzerin wird die registerNewUser(user: User)
-Funktion aus dem BackendService
in der onSubmit()
-Funktion in der register.component.ts
aufgerufen. Beide Funktionen sind bereits oben gezeigt. Auch hier könnte ein Bestätigungsdialog erstellt werden.
Login¶
Nachdem eine Nutzerin registriert ist, kann sie sich einloggen. Wir passen dazu die LoginComponent
an. Wir erstellen uns erneut ein reaktives Formular mit den Einwahldaten email
und password
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
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 |
|
1 2 3 4 5 6 7 |
|
Die LoginComponent
sieht dann so aus:
Jetzt passen wir noch den BackendService
an. Dort wird der Endpunkt POST http://localhost:3000/users/login/:email
verwendet, um zu überprüfen, ob die email
existiert und das Passwort korrekt ist. Die entsprechende loginUser()
-Funktion könnte so aussehen:
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 |
|
Nun implementieren wir nur noch die onSubmit()
-Funktion in der login.component.ts
, um die eingegebenen Daten korrekt an das Backend zu senden und mit der response
vernünftig umzugehen:
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 |
|
Es gibt zwei Möglichkeiten:
- Entweder die
email
existiert und daspassword
ist korrekt. Dann besteht dieresponse
aus dem gesamtenUser
-Datensatz (alsofirstname
,lastname
,role
usw.). Das ist derresponse
-Fall in dersubscribe()
-Funktion. - Oder das Backend sendet einen
403
-Status in einemHttpErrorResponse
-Objekt zurück. Das ist dererror
-Fall in dersubscribe()
-Funktion.
Je nachdem, ist die Nutzerin danach eingelogged oder nicht. Wir wollen in einem weiteren Service verwalten, ob eine Nutzerin eingelogged ist und wenn ja, dann in welcher Rolle.
Authentisierungs-Service¶
In dem Routing-Abschnitt haben wir auch Guards kennengelernt. Darin ahtten wir auch bereits einen einfachen AuthService
erstellt, der damals noch nur ein Dummy-Funktion hatte, um das Eingeloggtsein zu simulieren. Einen solchen AuthService
wollen wir nun auf Basis der Login-Informationen erweiteren und für unsere Guards verwenden. Dazu erstellen wir uns einen solchen Service:
ng g service shared/auth
Der AuthService
sieht zunächst wie folgt aus:
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 |
|
Wir haben einige Objektvariablen und Objektfunktionen erstellt, um zu verwalten, ob eine Nutzerin eingelogged ist und welche Rolle sie hat. Am Anfang ist loggedIn = false
und user = null
. Wir werden nun in der LoginComponent
diesen Service einbinden und die Funktionen entsprechend aufrufen:
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
Wenn das Login erfolgreich war, dann ist nun user
im AuthService
mit einem konkreten Objekt belegt und kann ausgelsen werden. Dazu sind ja bereits einige Funktionen im AuthService
vorgesehen. Wir werden diese Funktionen nun in einem Guard verwenden, mit dem wir den Zugriff auf die Komponenten steuern.
Guards für den Komponentenzugriff¶
In Routen absichern mit Guards haben wir bereits die Grundidee von Guards vorgestellt. Wir wollen diese hier anwenden und beschränken uns dabei auf den Guard-Typ CanActivate
. Wir wollen sicherstellen, dass die HomeComponent
nur aktiviert werden kann, wenn man eingelogged ist und die RegisterComponent
nur dann, wenn man als admin
eingelogged ist, um das Prinzip zu verdeutlichen. Wir erstellen uns also einen CanActivate
-Guard (im Ordner guards
):
ng g guard guards/authguard --implements CanActivate
Diesen AuthGuard
implementieren wir wie folgt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Dieser Guard gibt bei Aufruf der canActivate()
-Funktion ein true
zurück, wenn eine Nutzerin eingelogged ist (isAuthenticated()
aus dem AuthService
). Wenn niemand eingelogged ist, (wenn also isAuthentivacated()
ein false
zurückgibt), dann wird die aktuelle Route nach /login
umgeleitet. Wir fügen diesen Guard nun in die app-routing.module.ts
ein:
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 |
|
Wenn wir nun die Anwendung öffnen, dann kommen wir weder auf die HomeComponent
, noch auf die RegisterComponent
, sondern werden stets zur LoginComponent
geleitet. Erst wenn wir eingelogged sind, sind die beiden Komponenten erreichbar.
Wir erstellen noch einen weiteren Guard, um auch abzuprüfen, ob wir als admin
eingelogged sind und wollen mit diesem Guard die RegisterComponent
sichern.
ng g guard guards/adminguard --implements CanActivate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Dieses Mal wird geprüft, ob die Nutzerin eingelogged und in der admin
-Rolle ist. Diesen Guard fügen wir der /register
-Route hinzu. Nur ein admin
darf Registrierungen vornehmen (wird hier exemplarisch angenommen).
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 |
|
Wenn nun eine admin
-Userin eingelogged ist, kann sie die HomeComponent
, die RegisterComponent
und die LoginComponent
öffnen. Ist eine user
-Userin eingelogged, hat sie keinen Zugriff auf die RegisterComponent
, sondern nur auf die LoginComponent
und die HomeComponent
. Ist niemand eingelogged, kann nur die LoginComponent
verwendet werden.
Success
Wir haben eine (sehr einfache) Nutzerverwaltung implementiert. Eine Nutzerin kann sich registrieren und einloggen. Die Registrierungsdaten werden in der Datenbank gespeichert. Das Passwort wird verschlüsselt abgelegt. Jeder Nutzerin kann eine Rolle zugewiesen werden. Abhängig davon, ob jemand eingelogged ist bzw. in welcher Rolle sind die Komponenten unterschiedlich erreichbar. Dies wurde mit Guards realisiert. Für das Layout wurde Angular Material verwendet. Die Nutzerverwaltung ist noch sehr rudimentär. Es fehlt z.B. noch das Ausloggen. Es wäre auch gut, wenn die Nutzerin nach der Registrierung oder nach dem Einloggen eine entsprechende Erfolgsnachricht bekäme und z.B. auf die Home- oder die Login-Komponente weitergelitet würde. Die Konzepte für eine Dialoggestaltung, für die Erweitereung und Anbindung des Backends sowie für eine Weitereleitung auf eine ander Komponente wurden jedoch alle exemplarisch gezeigt.