Wir zeigen eine kleine Anwendung als vollständiges Beispiel. Es geht um die Erfassung und Anzeige von Ausgaben. Dazu wird eine kleine Nutzerverwaltung implementiert. Die Anwendung besteht aus einem Frontend und einem Backend. Zur persistenten Datenspeicherung wird eine MongoDB verwendet.
{"name":"backend","version":"1.0.0","description":"Buchungen","main":"server.js","scripts":{"watch":"nodemon ./server.js","test":"echo \"Error: no test specified\" && exit 1"},"author":"J. Freiheit","license":"ISC","dependencies":{"cors":"^2.8.5","dotenv":"^16.0.0","express":"^4.17.3","mongoose":"^6.2.3"},"devDependencies":{"nodemon":"^2.0.15"}}
Zuerst erstellen wir uns mithilfe von mongoose zwei Models, ein Model für User und das andere für die Items, die wir speichern wollen. Diese Models erstellen wir im models-Ordner:
constmongoose=require('mongoose');// items SchemaconstitemSchema=newmongoose.Schema({title:String,amount:Number,date:Date,user_id:String});// Exporting our model objectsmodule.exports=mongoose.model('Item',itemSchema);
Diese Modelle werden als Collections in der MongoDB verwendet. Dabei ist zu beachten, dass die Collections kleingeschrieben und als englischer Plural angelegt werden, also users und items.
Auf den Collections (Modelle) lassen sich in der MongoDB nun CRUD-Funktionen (Queries) ausführen, siehe dazu z.B. hier. Wir erstellen nun einzelne REST-Endpunkte in den Routen. Dazu legen wir im routes-Ordner eine user.route.js und eine item.route.js wie folgt an:
constexpress=require('express');constrouter=express.Router();constbcrypt=require('bcrypt');constUser=require('../models/user.model');// get all usersrouter.get('/',async(req,res)=>{constallUsers=awaitUser.find();console.log(allUsers);res.send(allUsers);});// post one userrouter.post('/',async(req,res)=>{constsaltRounds=10;letpwHash='';awaitbcrypt.genSalt(saltRounds,(err,salt)=>{bcrypt.hash(req.body.password,salt,(errHash,hash)=>{pwHash=hash;constnewUser=newUser({account:req.body.account,password:pwHash});console.log('newUser',newUser);newUser.save();res.send(newUser);});});});// get one user via account and passwordrouter.post('/login/:account',async(req,res)=>{try{constuser=awaitUser.findOne({account:req.params.account});letsendPw=req.body.password;letuserPW=user.password;bcrypt.compare(sendPw,userPW,(err,result)=>{if(result){console.log('Passwort korekt!');res.send(user);}else{console.log('falsches Passwort!');res.status(403);res.send({error:"Wrong password!"});}});}catch{res.status(404);res.send({error:"User does not exist!"});}});// get one user via accountrouter.get('/:account',async(req,res)=>{try{constuser=awaitUser.findOne({account:req.params.account});console.log(req.params);res.send(user);}catch{res.status(404);res.send({error:"User does not exist!"});}});module.exports=router;
constexpress=require('express');constrouter=express.Router();constItem=require('../models/item.model');// post one itemrouter.post('/',async(req,res)=>{constnewItem=newItem({title:req.body.titel,amount:req.body.betrag,date:req.body.datum,user_id:req.session.user_id})awaitnewItem.save();res.send(newItem);});// get all items for user_idrouter.get('/',async(req,res)=>{try{constitem=awaitItem.find({user_id:req.session.user_id});res.send(item);}catch{res.status(404);res.send({error:"User does not exist!"});}});// delete one item via idrouter.delete('/:id',async(req,res)=>{try{awaitItem.deleteOne({_id:req.params.id})res.status(204).send()}catch{res.status(404)res.send({error:"Item does not exist!"})}});module.exports=router;
Für die User werden also folgende Anfragen zur Verfügung gestellt:
Eine GET-Anfrage, die alle in der Collection users enthaltenen Einträge liefert.
Eine POST-Anfrage, der im body der Anfrage ein account und ein password übermittelt wird. Das Passwort wird mithilfe von bcrypt in einen Hashwert umgewandelt und dieser Hashwert wird gespeichert.
Eine POST-Anfrage unter /login/:account, wobei :account ein Parameter ist. Nach diesem account wird in der Collection users gesucht und der zugehörige Datensatz ermittelt. Im body der Anfrage wird das password mitgesendet. Mithilfe von bcrypt wird überprüft, ob das gesendete Passwort mit dem als Hashwert gespeicherten Passwort übereinstimmt.
Eine GET-Anfrage, der der account-Name als Parameter geschickt wird. Die _id des entsprechenden Datensatzes wird zurückgeschickt, ohne eine Passwortabfrage.
Für die Items werden folgende Anfragen zur Verfügung gestellt:
Eine POST-Anfrage zur Erzeugung eines item in der items-Collection. Die zum item gehörenden Daten werden im body der Anfrage übergeben.
Eine GET-Anfrage für alle items, die zu einer bestimmten user_id gehören. Diese user_id wird aus der Session ermittelt.
Eine DELETE-Anfrage, der eine _id als Parameter übergeben wird. Der Datensatz mit dieser _id wird gelöscht.
constexpress=require('express');constcors=require('cors');constmongoose=require('mongoose');constsession=require('express-session');require('dotenv').config();// Routes to Handle RequestconstuserRoute=require('./routes/user.route');constitemRoute=require('./routes/item.route');// Setup Express.js and Corsconstapp=express();app.use(express.json());app.use(cors());app.use(session({secret:'key that will sign the cookie',resave:false,saveUninitialized:false}));// API Routesapp.use('/user',userRoute);app.use('/item',itemRoute);constport=process.env.PORT||4000;app.listen(port,()=>{console.log('Connected to port '+port)})// connect to mongoDBmongoose.connect(process.env.DB_CONNECTION,{useNewUrlParser:true,useUnifiedTopology:true}).then(()=>{console.log('connected to DB');},err=>{console.error.bind(console,'connection error:')});
In der server.js wird die URL zur MongoDB aus der .env-Datei ausgelesen:
Wir können z.B. neue user anlegen. Dazu geben wir in Postman die URL http://localhost:4000/user ein und wählen als Anfragemethode POST. Im body (klicken Sie auf Body und raw und wählen Sie JSON) geben wir z.B. ein:
Wenn wir auf Send klicken, wird ein neuer Datensatz erzeugt, in dem das password einem Hashwert entspricht und der eine automatisch erzeugte _id besitz, z.B.
Öffnen Sie zur Kontrolle auch MongoDB Compass und verbinden Sie sich dort mit localhost:27017. In der booking-Datenbank erscheint in der Collection users der neue Eintrag:
Der neue Eintrag erscheint übrigens auch in der Konsole, da wir in der routes/user.route.js in Zeile 25 die entsprechende Konsolenausgabe implementiert hatten:
Ein gespeicherter user mit z.B. dem accounttest-account kann angefragt werden, indem eine POST-Anfrage an die URL http://localhost:4000/user/login/test-account gestellt und im body der Anfrage das Passwort übergeben wird.
Wir als account ein nicht existierender Nutzeraccount übergeben, z.B. http://localhost:4000/user/login/wrong-account, erscheint als Response
{"error":"User does not exist!"}
Der HTTP-Fehlercode ist 404 Not found.
Existiert zwar der account, jedoch ist das im body übergebene Passwort falsch, erscheint als Response
Um die user_id zu einem gegebenen account zu ermitteln, wird eine GET-Anfrage gestellt, der der account als Parameter der URL übergeben wird, also z.B.
http://localhost:4000/user/test-account
Als Antwort erhält man "621610740784dc1af03613d1" mit dem HTTP-Code 200 OK. GET-Anfragen können auch direkt im Browser gestellt werden, indem man einfach die URL eingibt.
Alle in der Datenbank gespeicherten user können über die GET-Anfrage http://localhost:4000/user ermittelt werden. Als Response wird ein Arry zurückgegeben, das alle user als JSON enthält, z.B.
So ist beispielsweise das Erstellen eines neuen Items so definiert, dass title, amount und date aus dem body der Anfrage entnommen werden (siehe Zeilen 9-11 in item.route.js), die user_id der Nutzerin, die dieses Item erstellt, wird jedoch aus der Session ausgelesen.
Zunächst einen Service, über den wir später verwalten, ob eine Nutzerin eingelogged ist oder nicht. Wir geben zunächst immer ein true zurück, damit alle Routen angewählt werden können.
Der AuthGuard gibt für die canActivate()-Funktion ein true zurück, wenn die Nutzerin eingelogged ist. Wenn nicht, wird die Route nach /login umgeleitet und ein false zurückgegeben.
Die Routen werden wie folgt definiert. Wenn Sie im auth.service anstelle von return true ein false zurückgeben, können die Komponenten Home, Input und Table nicht ausgewählt werden, sondern Sie werden dann stets auf die LoginComponent umgeleitet.
In der NavComponent werden die routerLink für die einzelnen Menüeinträge definiert und es wird die <router-outlet>-Direktive eingebunden, in die dann dynamisch die jeweils aufgerufene Komponente eingebunden wird.
<form[formGroup]="loginForm"novalidate(ngSubmit)="onSubmit()"><mat-cardclass="login-card"><mat-card-header><mat-card-title>Login</mat-card-title></mat-card-header><mat-card-content><divclass="row"><divclass="col"><mat-form-fieldclass="full-width"><inputmatInputplaceholder="User name"formControlName="account"><mat-error*ngIf="loginForm.controls['account'].hasError('required')"> user name is <strong>required</strong></mat-error></mat-form-field></div></div><divclass="row"><divclass="col"><mat-form-fieldclass="full-width"><mat-label>Enter your password</mat-label><inputmatInput[type]="hide?'password':'text'"formControlName="password"><buttonmat-icon-buttonmatSuffix(click)="hide =!hide"[attr.aria-label]="'Hidepassword'"[attr.aria-pressed]="hide"><mat-icon>{{hide ? 'visibility_off' : 'visibility'}}</mat-icon></button><mat-error*ngIf="loginForm.controls['password'].hasError('required')"> password is <strong>required</strong></mat-error></mat-form-field></div></div></mat-card-content><mat-card-actions><buttonmat-raised-buttoncolor="primary"type="submit"[disabled]="!loginForm.valid">Login</button></mat-card-actions></mat-card></form>
Im Formular wird der Datepicker von Angular Materail verwendet. Außerdem wollen wir die deutsche Darstellung des Datums und benötigen dafür DatePipe. Diese Module müssen zunächst in der app.module.ts importiert werden:
<form[formGroup]="inputForm"novalidate(ngSubmit)="onSubmit()"><mat-cardclass="booking-card"><mat-card-header><mat-card-title>Buchung</mat-card-title></mat-card-header><mat-card-content><divclass="row"><divclass="col"><mat-form-fieldclass="full-width"><inputmatInputplaceholder="title"formControlName="titel"><mat-error*ngIf="inputForm.controls['titel'].hasError('required')"> title is <strong>required</strong></mat-error></mat-form-field></div></div><divclass="row"><divclass="col first"><mat-form-fieldclass="full-width"><inputmatInputtype="number"onfocus="this.select()"placeholder="amount"formControlName="betrag"><mat-iconmatSuffixclass="smaller">euro_symbol</mat-icon></mat-form-field></div><divclass="col"><mat-form-fieldclass="full-width"><mat-label>date</mat-label><inputmatInput[matDatepicker]="datum"formControlName="datum"><mat-datepicker-togglematSuffix[for]="datum"></mat-datepicker-toggle><mat-datepicker#datum></mat-datepicker></mat-form-field></div></div></mat-card-content><mat-card-actions><buttonmat-raised-buttoncolor="primary"type="submit">Buchen</button></mat-card-actions></mat-card></form>