Node und express: Intermediate

Layers

Layers

häufige Strukturierung mit "Layers" in Web-Anwendungen:

  • Controller Layer (HTTP)
  • Service Layer (Geschäftslogik / business logic)
  • Model Layer (Datenbankanbindung)

Layers

Controller: Empfängt einen HTTP-Request, ruft eine Service-Funktion für die Verarbeitung auf, sendet die Response

Service: Wird durch einen Controller aufgerufen; kann auf andere Services und Models zugreifen; Kann Daten an den Controller zurückliefern; beinhaltet die Haupt-Logik (Geschäftslogik)

Model: Stellt für Services Datenbankzugriffe zur Verfügung

Layers

oft entspricht eine Route (HTTP-Anfrage) direkt einem Modell (Datenbank-Tabelle / -Abfrage)

z.B. in einer Shopping-Anwendung:

/products?category=phones&max_price=500

entspricht:

SELECT * FROM products WHERE category = 'phones' AND price <= 500;

Layers

manchmal kann einiges im Service Layer geschehen:

Beispiel: Senden einer neuen Nachricht via POST-Request an /messages

messageService könnte folgendes verwenden:

  • authService (zum Authentifizieren / Authorisieren des Versenders)
  • userModel (um den Empfänger zu finden)
  • spamDetectionService
  • messageModel (um die Nachricht zu speichern)
  • notificationService (um die Empfänger*innen zu benachrichtigen - z.B. via SMS / email)

Layers

häufige Struktur in Express-Anwendungen:

  • Routing Layer: empfängt HTTP-Anfragen und leitet sie an einen Controller weiter; wendet eventuell sogenannte Middleware an
  • Controller Layer
  • Service Layer
  • Model Layer

Layers

kann für einfachere Projekte vereinfacht werden, z.B.:

  • Routing / Controller / Service Layer
  • Model Layer

Source Code Struktur

Beispiel für eine typische Source Code Struktur:

  • server.js
  • db.js (Datenbankanbindung)
  • routes/
  • controllers/
  • services/
  • models/

Routing

Routing

Wir können unsere Routendefinitionen mittels Router-Objekten strukturieren

Routing

alles an einem Ort:

app.get('/accounts', () => {
  // ...
});
app.get('/accounts/:id', () => {
  // ...
});
app.post('/accounts', () => {
  // ...
});

app.get('/transactions', () => {
  // ...
});
// ...

Routing

saubere Struktur:

app.use('/accounts', accountsRouter);
app.use('/transactions', transactionsRouter);

Routing

Definition von accountsRouter in separater Datei (z.B. routes/accounts.route.js)

import { Router } from 'express';

export const accountsRouter = Router();

accountsRouter.get('/', () => {
  // ...
});
accountsRouter.get('/:id', () => {
  // ...
});

Authentifizierung und Authorisierung: Theorie

Authentifizierung und Authorisierung: Theorie

Themen:

  • Terminologie
  • Roles und Permissions
  • Session-Tokens
  • Passwörter und Passwort-Hashes

Terminologie

  • Authentifizierung: Kommt diese Anfrage tatsächlich von User xyz?
  • Authorisierung: Darf User xyz diese Daten sehen / modifizieren?

Rollen und Berechtigungen

Beispiele für Rollen und Berechtigungen auf einer Nachrichtenseite:

  • Gäste können die ersten 500 Zeichen eines Artikels sehen
  • User können vollständige Artikel sehen; User können Kommentare sehen; User können eigene Kommentare erstellen, bearbeiten und löschen
  • Moderatoren können Artikel erstellen, ändern und löschen; Moderatoren können Kommentare löschen

Session-Tokens

Typischer Auth-Ablauf mit Tokens:

  • Client sendet Authentifizierungsdaten (z.B. Username + Passwort) zum Server
  • Server überprüft Daten
  • Server sendet ein geheimes Session-Token (als String) für diese Session zurück
  • Client kann mit diesem Token auf bestimmte Seiten / APIs zugreifen
  • Token wird typischerweise nach einer bestimmten Zeiten oder nach dem Logout ungültig

Session-Tokens

Session-Tokens werden vom Client zum Server in HTTP-Headern gesendet:

  • Authorization Header (benötigt JavaScript am Client)
  • Cookie Header

Session-Tokens

Beispiel am Client: Einloggen via Benutzername und Passwort, Erhalten des Tokens:

const loginRes = await fetch('/api/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    username: 'foo',
    password: 'bar',
  }),
});
const loginData = await loginRes.json();
const token = loginData.token;

Session-Tokens

Beispiel am Client: mit Hilfe des Tokens auf bestimmte Daten zugreifen:

const notesRes = await fetch('/api/notes', {
  headers: { Authorization: `Bearer ${token}` },
});
const notes = await notesRes.json();

Passwörter und Passwort-Hashes

  • schlecht: Speichern von Passwörtern direkt in der Datenbank
  • besser: Speichern von Passwort-Hashes
  • ideal / standard: Speichern von Passwort-Hashes die "salted" und "peppered" sind

Passwörter und Passwort-Hashes

ursprüngliche Daten:

name    | password
--------+--------------------
alice   | 123456
bob     | 123456
charlie | abc123

Passwörter und Passwort-Hashes

Daten mit Passwort-Hashes:

name    | password hash
--------+---------------------------------
Alice   | e10adc3949ba59abbe56e057f20f883e
Bob     | e10adc3949ba59abbe56e057f20f883e
Charlie | e99a18c428cb38d5f260853678922e03

Passwörter und Passwort-Hashes

Hashing-Algorithmen - sortiert von am sichersten zu nicht sicher:

  • Argon2
  • scrypt
  • bcrypt
  • PBKDF2
  • MD5

Authentifizierung und Authorisierung in Express

Beispiel: Login Handler

app.post('/api/login', async (req, res) => {
  const success = await authService.validateLogin(
    req.body.username,
    req.body.password
  );
  if (success) {
    const token = authService.createRandomToken();
    await authService.saveSession(req.body.username, token);
    res.json({ token: token });
  } else {
    res.status(401).send();
  }
});

Beispiel: Data Handler

app.get('/api/notes/:id', async (req, res) => {
  const authHeader = req.header('Authorization');
  const token = authHeader.split(' ')[1];
  const session = await authService.getSession(token);
  if (!session) {
    // not authenticated
    res.status(401).send();
    return;
  }
  const note = await notesService.getNote(req.params.id);
  const role = await authService.getRole(session);
  if (role === 'user' && session.userId === note.userId) {
    res.json(note);
  } else if (role === 'admin') {
    res.json(note);
  } else {
    // not authorized
    res.status(403).send();
  }
});

Konfiguration mittels Umgebungsvariablen

Konfiguration mittels Umgebungsvariablen

Zugangsdaten (z.B. für Datenbanken) und Konfiguration werden üblicherweise via Umgebungsvariablen bereitgestellt

Zugangsdaten sollten nicht unter Versionskontrolle stehen

.env-Datei

verbreitete Möglichkeit, um Zugangsdaten bereit zu stellen: speichern in einer Datei namens .env, laden als Umgebungsvariablen mittels dotenv

Stelle sicher, dass .env nicht unter Versionskontrolle steht (füge es zur Datei .gitignore hinzu)

.env-Datei

Beispiel für .env-Datei:

PORT=3000
NODE_ENV=production
DB_URL=mongodb+srv://...
AUTH0_DOMAIN=xxx.eu.auth0.com

laden in JavaScript:

import dotenv from 'dotenv';

dotenv.config();

const PORT = process.env.PORT;
const NODE_ENV = process.env.NODE_ENV;

NODE_ENV

Umgebungsvariable NODE_ENV: spielt z.B. bei express eine wichtige Rolle

in Produktivumgebungen sollte immer NODE_ENV=production gesetzt sein - ansonsten werden z.B. dem Endnutzer JavaScript-Fehlermeldungen im Detail angezeigt (mit Stack Traces)

Hosting

Hosting

Hosting Provider mit kostenlosen Optionen:

  • glitch.com
  • render.com
  • fly.io
  • ...