SMD-CMS

Das SMD-CMS ist ein Api-Backend für diverse Anwendungen, die von der SMD entwickelt wurden. Es ist in TypeScript geschrieben und nutzt ein eigenes Framework, welches Express.js ansatzweise ähnelt.

Projekt Struktur

  • app.ts: Entry point für die Anwendung
  • appspace.ts: Beinhaltet den Appspace
  • classes/: Diverse Hilfsklassen
  • config/: Konfigurationsdateien
  • flower/: Beinhaltet die Flower
  • jobs/: Scripte für diverser Cron-Jobs
  • models/: Modelle für den Datenzugriff
  • views/: Views/Controller zur Darstellung von xml oder json Responses
  • mailtemplates/: Templates für dem Mailversand
  • __tests__/: Jest-Tests

Versionen

Es sind mehrer Versionen der API in Nutzung die sich teilweise unterscheiden.

2.2.0 styria.com
2.3.0 skillbase
2.5.0 Votingplattformen, clicktracking

Grundlegende Konzepte

Appspace

Der Appspace ist eine Sammlung von diversen Services und Helpern, die in der gesamten Laufzeit der Anwendung genutzt werden. Dies ist beispielsweise die Datenbankverbindung, oder der Logger. Das hilft dabei Requests selbst performant zu machen, da nicht ständig Klassen oder Verbindungen neu initialisiert werden müssen, oder um einen permanenten State zu halten. Der Appspace wird in der Datei src/appspace.ts erstellt und in der Datei src/app.ts initialisiert.

Flow

Jeder Request durchläuft das System in einem sogenannten 'Flow'. Ein Flow besteht dabei aus mehreren Schritten, die in einer bestimmten Reihenfolge abgearbeitet werden. Jeder Schritt kümmert sich um einen gewisssen Aspekt des Requests, wie z.B. das Parsen der Request-Parameter, das Parsen der URLs, oder das Rendern der View.

Jeder dieser Schritte wird einem sogenanten Flower abgebildet. Jeder Flower ist in seinem eigenen Modul unter src/flower/ abgelegt und exportiert die Funktion Flow, sowie einen String, der den Namen des Flowers repräsentiert.

export const name = 'Headers';
export const flow = async function (req: http.IncomingMessage,
                                    res: http.ServerResponse,
                                    flowspace: Flowspace,
                                    next: Function) {
    ... // Do something

    next();
}

export default { flow, name };

Durch den Aufruf der next() Funktion wird der nächste Flower im Flow aufgerufen. Dadurch kann ein Flower auch den Flow unterbrechen, indem die next() Funktion nicht aufgerufen wird. Der Header-Flower gibt den Request bei einem Options Request zum Beispiel direkt zurück, ohne den nächsten Flower aufzurufen.

Flowspace

Im Gegensatz zum Appspace ist der Flowspace nur für die Dauer eines Requests gültig. Das wäre zum Beispiel eine User-Session, eine hochgeladene Datei, oder die Route der URL, welche von späteren Flowern beutzt werden können. Der Flowspace wird in der Datei src/app.ts erstellt und an jeden Flower übergeben.

Models

Ein Model ist ein Klasse welche den Zugriff auf die Datenbank kapselt. Jedes Model ist in einem eigenen Modul unter src/models/ abgelegt und exportiert eine Klasse, die die Datenbankzugriffe kapselt. Models werden großteils von Views verwendet um Daten aus der Datenbank zu laden.

class CareerModel {
  getAllCareers() {
    return appspace.db
      .query('careers')
      .execute()
      .then((res: Array<CareerRecord>) => res);
  }
}

export const Career = new CareerModel();

Views

Eine View ist ein Modul, welches die Antwort auf einen Request generiert, oder Controller-Funktionen übernimmt. Jedes Modul wird in src/views abgelegt und exportiert dabei einen String mit seinem Namen und diverse Funktionen die die Methoden des Request-Objektes darstellen. Das View hat dabei Zugriff auf den Flowspace und kann damit Daten die von anderen Flowern zuvor gesezt wurden, verwenden.

export const get = async function (req: http.IncomingMessage, res: http.ServerResponse, flowspace: Flowspace) {
  // Do stuff
  return message;
};

Mögliche Funktionen sind get, post, put, del und patch

Der Rückgabe Wert wird als Nachricht vom View-Flower interpretiert und zurückgegeben. Handelt es sich dabei um ein Objekt, wird es automatisch in JSON oder XML konvertiert.

Wird im Zuge des Flows ein Stauscode gesetzt, wird dieser in den Header des Response-Objektes übernommen, und im Fallen von 404, 403 oder 500 wird die Nachricht durch eine Standardnachricht ersetzt.

Konfiguration

>= 2.2.0

Die Konfiguration der Anwendung erfolgt über diverse Dateien im src/config/ Verzeichnis.

Datenbankverbindungen, Mailserver, Routen, Jobs, Session-Keys, sowie diverse andere Einstellungen können hier vorgenommen werden. Zudem gibt es die Möglichkeit, eine 'env' zu setzten die diverse Einstellungsblöcke modifiziert.

<= 2.2.0

Da es sicherheitstechnisch ein Problem darstellt Passwörter und andere sensible Daten in der Konfigurationsdatei zu speichern, werden ab Version 2.2 großteils Umgebungsvariblen verwendet. In src/config findet sich nur noch die Routen, Jobs und neu hinzugekommene Namespaces für Websockets.

Querybuilder

Der Querybuilder ist ein Hilfsmittel um das SQL der Datenbankzugriffe zu abstrahieren. Dafür benutzt er ein Builderpattern um eine Datenabfrage darzustellen. Die einzelnen Methoden des Builders geben dabei immer den Builder zurück, um Methodenketten zu ermöglichen.

Jegliche Datenabfrage passiert dabei über diesen Layer, um die Datenbankzugriffe zu vereinheitlichen und zu vereinfachen. Dies ermöglicht es auch, die Datenbankverbindung und gar Datenbank-Software zu wechseln, ohne dass die Anwendung angepasst werden muss.


query(table: string), execute()

Jede Abfrage beginnt mit query(), welches den Tabellennamen als Parameter erwartet. Ohne weitere Parameter generiert sich ein SELECT * FROM table Statement.

execute() führt die Abfrage schliesslich aus und gibt ein Promise zurück, welches das Ergebnis der Abfrage enthält.

// SELECT * FROM table
appspace.db
  .query('table')
  .execute()
  .then((res: Array<Type>) => res);

HIER NOCH SCHEMA ERGÄNZEN


select(), update(), insert(), delete(bool)

Die Methoden erlauben das Setzen des Statement-Typs. select() ist dabei der Standard.

// DELETE FROM table WHERE id = 1
appspace.db.query('table').delete().where('id', 1).execute();

// UPDATE table SET name = 'Hallo Welt' WHERE id = 1 RETURNING *
appspace.db.query('table').update().set('name', 'Hallo Welt').where('id', '=', 1).execute();

Wird bei delete() true übergeben, wird ein softdelete durchgeführt. Im Grunde eine kurzschreibweise für .update().set('deleted', true)

// UPDATE table SET deleted = true WHERE id = 1 RETURNING *
appspace.db.query('table').delete(true).where('id', '=', 1).execute();

noreturn()

Standartmäßig gibt der Querybuilder das Ergebnis der Abfrage zurück. Mit noreturn() kann das unterdrückt werden.

// UPDATE table SET name = 'Hallo Welt' WHERE id = 1 RETURNING *
appspace.db.query('table').update().set('name', 'Hallo Welt').where('id', '=', 1).execute();

// UPDATE table SET name = 'Hallo Welt' WHERE id = 1
appspace.db.query('table').update().noreturn().set('name', 'Hallo Welt').where('id', '=', 1).execute();

fields(fields: string | Array<string>)

Setzt ein oder mehrere Felder die abgefragt werden sollen.

// SELECT id, name FROM table
appspace.db.query('table').fields(['id', 'name']).execute();

// SELECT id, name FROM table
appspace.db.query('table').fields('id').fields('name').execute();

Ab 2.3.0 kann auch field as alias verwendet werden, um Aliase zu benutzen.

// SELECT id as identifier FROM table
appspace.db.query('table').fields('id as identifier').execute();

where(field: string, symbol: FieldValue, value?: FieldValue, operator?: string)

Generiert eine WHERE-Klausel.

// SELECT * FROM table WHERE id = 1
appspace.db.query('table').where('id', '=', 1).execute();

Mit dem optionalen Parameter operator kann der Operator für die WHERE-Klausel gesetzt werden. Standardmäßig ist es AND.

// SELECT * FROM table WHERE id = 1 AND name = 'test'
appspace.db.query('table').where('id', '=', 1).where('name', '=', 'test').execute();

// SELECT * FROM table WHERE id = 1 OR name = 'test'
appspace.db.query('table').where('id', '=', 1).where('name', '=', 'test', 'OR').execute();

Ab 2.3.0 kann auch ein Shorthand für "=" verwendet werden, in dem man den Operator einfach weglässt.

// SELECT * FROM table WHERE id = 1
appspace.db.query('table').where('id', 1).execute();

parentesisOpen(operator?: string), parentesisClose()

Diese Methoden erlauben das Setzen von Klammern in WHERE-Klauseln.

appspace.db.query('table').where('id', 1).parentesisOpen().where('name', 'test').where('name', '=', 'test2', 'OR').parentesisClose().execute();
// SELECT * FROM table WHERE id = 1 AND (name = 'test' OR name = 'test2')

set(field: string | { [propName: string]: any }, value?: any)

Diese Methode erlaubt das Setzen von Feldern für Update und Insert. Dabei kann entweder ein Key-Value-Pair übergeben werden, oder ein Object.

appspace.db.query('table').insert().set('name', 'test').execute();

const record = {
  name: 'test',
  age: 1234,
  isBool: true,
};

appspace.db.query('table').insert().set(record).execute();

unset(field: string)

Die gegenteilige Methode um ein Feld welches mit .set() gesetzt wurde wieder zu entfernen.

const record = {
  name: 'test',
  age: 1234,
  isBool: true,
};

appspace.db.query('table').insert().set(record).unset('age').execute();

join(collection: string, field1: string, field2: string, modifier?: string)

Mit dieser Methode lassen sich SQL-Joins abbilden. Standartmäßig wird von einem INNER JOIN ausgegangen, via modifier lassen sich aber auch andere Joins bilden.

Bei der Eingabe der Felder kann die '.'-Notation verwendet werden um den Feldnamen auf die jeweilige Tabelle zu beziehen.

appspace.db.query('table').join('table2', 'id', 'id').execute();

appspace.db.query('table').join('table2', 'table2.id', 'table.id').execute();

appspace.db.query('table').join('table2', 'id', 'id', 'OUTER').execute();

appspace.db.query('table as t').join('table2 as t2', 't.id', 't2.id').where('t.id', 1).execute();

aggregate(func: AggregateFunction, field: string)

Hiermit lassen sich Aggregate-Funktionen nutzen. Gültige Werte sind MAX, MIN, COUNT, SUM und AVG.

field gibt dabei dann an auf welches Feld die Funktion angewendet werden soll.

// SELECT * AS cnt FROM table;
appspace.db.query('table').aggregate('COUNT', '* as cnt').execute();

distinct(fields: string | Array<string>)

Mit dieser Methode lassen sich DISTINCT-Abfragen abbilden.


order(field: string, asc?: string)

Hiermit kann man ORDER BY Abfragen abbilden. Standardmäßig wird von einer absteigenden Sortierung ausgegeangen.

// SELECT * FROM table ORDER BY name
appspace.db.query('table').order('name').execute();

// SELECT * FROM table ORDER BY name ASC
appspace.db.query('table').order('name', 'ASC').execute();

group(field: string)

Implementiert die GROUP BY - Klausel


limit(limit?: number, offset?: number)

Hiermit kann die LIMIT - Einschränkung benutzt werden um die Rückgabe der Datenbank kleiner zu halten.

// SELECT * FROM table WHERE id > 500 LIMIT 20
appspace.db.query('table').limit(20).execute();

// SELECT * FROM table WHERE id > 500 LIMIT 20 OFFSET 500
appspace.db.query('table').limit(20, 500).execute();

fuzzy(field?: string)

LIMIT <n> OFFSET <m> kann bei großen Tabellen zu Perfomanceproblemen führen. Ein Workaround dafür ist, dass man ein WHERE x > m benutzt. Dies erfordert allerdings, dass die Daten sortiert oder in einer festen Reihenfolge sind. Wenn die Genauigkeit der Daten nicht relvant ist, aber die Geschwindigkeit der Abfrage wichtig kann fuzzy() in Kombination mit Limit benutzt werden um diesen Workaround zu nutzen.

// SELECT * FROM table WHERE id > 500 LIMIT 20
appspace.db.query('table').limit(20, 500).fuzzy('id').execute();

debug()

Gibt das generierte SQL-Statement in der Konsole aus. Dabei wird es aber dennoch ausgeführt.

Ab 2.3.0 werden zusätzlich noch Profiling Informationen gesammelt und am Ende im der Konsole ausgegeben.

Models

Models sind eine Klasse die diverse Queries aus dem Querybuilder zusammenfassen. Hinergrund ist den Code in den Views kompakter zu halten, und Abfrage-Logik zu abstrahieren. Der Grundaufbau eines Models folgt dabei grundlegend immer dem selben Schema.

models/examples.ts

// appspace brauchen wir da dieser die Datenbank-Verbindung hält
import { appspace } from '../appspace.js';

// In dem Model befindet sich im Normalfall eine Type-Definition die im Hintergrund die Datenbank-Tabelle abzeichnet. Dies gibt die Möglichkeit Typesafety zu behalten
interface ExampleRecord {
  id: int;
  name: string;
  example: bool;
}

// Die Klasse auf dessen Methoden dann zugegriffen wird
class ExampleModel {
  getExamples() {
    return appspace.db
      .query('examples')
      .execute()
      .then((res: Array<ExampleRecord>) => res); // Das Ergebnis der Abfrage wird zurück gegeben
  }

  getExample(id: int) {
    return appspace.db
      .query('examples')
      .where('id', int)
      .execute()
      .then((res: Array<ExampleRecord>) => res[0]); // Postgress und Mysql geben beide auch wenn nur eine Row zurückkommt ein Array aus Rows. Deshalb geben wir nur das erste Element zurück
  }
}

// Exportieren und Instanzieren der Klasse
export const Examples = new ExampleModel();

views/###.ts

import { Examples } from '../models/example.js';

// ...

await Examples.getExamples();

await Examples.getExample(1);

// ...

Systemmodels

Einge Models sind für die Funktionalität des Systemes essentiell, ab Version x.x.x befinden sich diese in einem eigenen Unterordner unter models/system.

Diese Modelle wären da:

  • acl.ts: Datenbank-Abfragen und Funktionalitäten bezüglich der Rechteverwaltung
  • files.ts: Hilfsfunktionen um Files zu verwalten
  • media.ts: Hilfsfunktionen um Media-Einträge zu verwalten
  • token.ts: Hilfsfunktionen für das Auth-System. Verwaltet Tokens, validiert und issued diese. Stellt zudem die Payloads für JWT zusammen.
  • user.ts: Hilfsfunktionen für das Usermanagement und verschlüsseln von Passwörtern.

Flower

Flower sind Funktionen welche einen Request be- und verarbeiten. Ein eingehender Request durchläuft entsprechend eine Reihe von Flowern.

export const name = 'Toller Name';
export const flow = async function (req: http.IncomingMessage, res: http.ServerResponse, flowspace: Flowspace, next: Function) {
  // ... Bearbeite den Request

  // Springe zum nächsten Request

  next();
};

export default { flow, name };

Flower finden sich im Ordner src/flowers. In der app.ts werden Flower definiert. Gewisse Anwendungsfälle für die API können so auch Flower auskommentieren und gar nicht nutzen um Requests schlank und performant zu halten.

const flow = new Flow(); // Anlegen des Flows

flow.use(Headers); // Hinzufügen einzenler Flower
// flow.use(Static);
flow.use(BodyData);

switch (
  appspace.featureset.login // Auch Conditional
) {
  case 'ad':
    flow.use(Auth_AD);
    break;
  case 'session':
  default:
    flow.use(Auth_USER);
    break;
}

flow.use(Router);
flow.use(View);
flow.listen(); // Der Flow ist nun konfiguriert und startet hier den http-server um auf requests zu horchen

Beim Durchlaufen der Funktion kann der Flower mit dem request und response arbeiten. Der flowspace ist ein Objekt in welchem Informationen für spätere Flower hinterlegt werden können. Der Router beispielsweise hinterlegt dort den Namen der View die der View-Flower dann aufruft.

Wenn der Flower mit der Verarbeitung fertig ist, wird die next()-Funktion aufgerufen, die den Request zum nächsten Flower übergibt. Wird next() nicht aufgerufen, endet der Flow an der Stelle. Den Umstand nutzt zum Beispiel der Flower der sich um das Setzen der Header kümmert, um den OPTIONS-Request direkt zu beantworten ohne den Flow weiter gehen zu müssen.

Eine Besonderheit ist noch dass ein Flower nachfolgende Flower deaktivieren kann. In flowspace.skip findet man ein Array<string> in dem Namen für Flower die übersprungen werden hinterlegt werden. Der Session-Flower nutzt das um den Router zu überspringen, wenn es sich um einen /auth Request handelt.

export const flow = async function (req: http.IncomingMessage, res: http.ServerResponse, flowspace: Flowspace, next: Function) {
  // Catch Auth-URLS

  if (req.url?.startsWith('/auth')) {
    logger.info('Auth-URL detected');
    switch (
      req.url
      // ...
    ) {
    }
    flowspace.skip.push('Router v2');
  }

  flowspace.session = await createSession(req, res, flowspace);
  next();
};

Routing

Das Routing erfolgt über den Router-Flower. Dieser nutzt die Konfiguration in src/config/routes.ts um die URL auf die entsprechende View zu mappen. Der Router-Flower kümmert sich dann um das auslesen der URL-Parameter und das schreiben der richtigen View in den flowspace. Die Views selbst sind dann dafür zuständig die Antwort zu generieren.

// config/routes.ts
import { TestView } from '../views/test';

export const routes: Array<RouteNode> = [
  {
    path: 'test/:id:/edit',
    view: TestView,
  },
];

// views/test.ts:
export const name = 'TestView';

export const get = async function (req: http.IncomingMessage, res: http.ServerResponse, flowspace: Flowspace) {
  console.log(flowspace.params?.id);
};

Wie in dem Beispiel ersichtlich können die URL-Parameter über flowspace.params ausgelesen werden.

Zusätzlich können auch statische Parameter gesetzt werden, die dann in flowspace.params zu finden sind.

  {
    path: 'test/:id:/edit',
    view: TestView,
    params: { mode: 'edit' }
  }

//...
export const get = async function (req: http.IncomingMessage, res: http.ServerResponse, flowspace: Flowspace) {
    console.log(flowspace.params?.mode); // edit

Sessions

Die API hat eine Nutzer- und Rechteverwaltung integriert. In der Skillbase-Verson gibt es neben der eigenen Authentifizierung via JWT-Tokens auch die Möglichkeit Active Directory zu nutzen.

Alle Endpunkte die mit Authetifizierung zu tun haben finden sich unter /auth. Es gibt /auth/login, /auth/logout und /auth/refresh. Die Logik findet sich dazu als Flower in src/flowers/auth oder in neueren Versionen die AD können unter src/flowers/auth/activedirectory.ts und src/flowers/auth/session.ts gefunden werden.

Auth

Die Authentifizierung erfolgt über JWT-Tokens. Bei einem Login wird ein Token-Paar erstellt, welches der Client dann nutzen kann um sich für zukünftige Requests zu authentifizieren. Es gibt einen Access- und einen Refresh-Token. Der Access-Token ist kurzlebig(5 Minuten) und wird für jeden Request benötigt. Der Refresh-Token ist langfristig gültig und wird genutzt um ein neues Token-Paar zu generieren.

Um Session-Highjacking zu vermeiden ist ein Refresh-Token genau nur für EINE Erstellung gültig. Sollte ein Refresh-Token also mehr als einmal genutzt werden, wird alles in Zusammenhang mit diesem Token invalidiert. Damit hat ein Angreifer ein maximales Zeitfenster von 5 Minuten um den Access-Token zu missbrauchen.

Die aktuellen Tokens werden in der Tabelle activetokens hinterlegt. Ein Job in der API löscht in gewissen Abständen abgelaufene Tokens.

User- und Passwort-Informationen werden in der Tabelle users hinterlegt. Passwörter werden dabei mit einem 512 Byte langen Salt und SHA-256 gehasht. Siehe Model User für genauere Implementierungs-Details

Active Directory

Für die Skillbase kann man sich auch via Active-Directory authentifizieren. Generell passiert die meiste Logik hier Client-seitig. Der Client generiert über die jeweilige Azure-AD-Instanz ein Token, welches dann an die API gesendet wird. In der API ist in der Tabelle activedirectory die OIDs die AD intern nutzt mit dem User-IDs des Systemes verknüpft. Nachdem der Token verifiziert wurde, gibt die API den User dann frei.

Die Verknüpfung der OIDs mit den Usern passiert über Invite-Keys die generiert werden. Sobald dann ein User sich mit seinem AD-Token anmeldet, wird der Invite-Key genutzt um den User zu verknüpfen.

ACL

Die API besitzt intern eine Rechteverwaltung um gewissen Endpoints und Content abzusichern. Dabei ist das Model ACL und darunter liegend die Tabellen aclroles, aclrihgts, aclroles_aclrights und user_role beteiligt.

Es können Rollen erstellt werden, denen dann bestimmte Rechte zugewiesen werden. Ein solches Recht ist ein simpler String wie etwa user.edit, grade.edit oder dergleichen. Den Usern selbst werden nun Rollen zugeordnet. Die Kumulation der einzelnen Rechte, ergibt dann die finalen Rechte des Users. Dadurch ist das Prinzip gewahrt, dass Rechte auf Whitelistung-Basis vergeben werden.

Da allerdings gewisse Endpoints auch ohne Authorisierung genutzt werden können, muss die Abfrage der Rechte explizit in den Views erfolgen. Dazu stellt der flowspace eine Funktion hasRight zur Verfügung, die die Rechte des aktuellen Users des Requests abfragt.

if (flowspace.session?.hasPerms('admin')) {
  // ... Do stuff only admins shall do
}

acl.bypass

Das recht acl.bypass umgeht immmer Rechteabfragen, beziehungsweise geben diese immer true zurück. Das hat den Hintergrund um Root-Accounts die Rechte zu geben, die sie benötigen um die Rechteverwaltung zu können. Beispielsweise gibt es in der Skillbase die Möglichkeit der "Personifizierung", die es zu Debug-Zwecken erlaubt die Anwendung als anderer User zu nutzen. Dafür wird das acl.bypass-Recht benötigt.

Tokens & Content-Sicherheit

Der JWT-Token den die API ausgibt, enthält die Rollen des Users. Diese können dann in der Anwendung genutzt werden um die Rechte des Users zu Anzeigezwecken zu kennen. So kann zB ein Adminmenü nur angezeigt werden, wenn der User auch die Rechte dazu hat. Die Rechte die im JWT enthalten sind, sind allerdings nur die Rechte die der User zum Zeitpunkt des Logins hatte. Sollten sich die Rechte des Users ändern, muss er sich neu einloggen. Zusätzlich werden diese Rechte in keinster Weise von der API selbst benutzt um Freigaben zu bestimmen. Dafür wird immer die Datenbankabfrage genutzt, um zu verhindern, dass ein User sich Rechte selbst gibt.

Daten sollten niemals nur Client-seitig versteckt werden. Die API sollte immer die letzte Instanz sein, die entscheidet ob ein User Zugriff auf Daten hat oder nicht, und auch wirklich nur die Daten ausgeben die der User auch sehen darf. Gerade bei der Skillbase macht dies einige Views komplexer da die Datenbankabfragen teils erst gefiltert werden müssen, bevor sie an den Client gehen.

Medien & Statische Daten

Das SMD-CMS hat eine Medienverwaltung integriert. Diese erlaubt es Dateien hochzuladen, speichert deren Metadaten, und kann diese auch statisch ausliefern. Zudem gibt es noch die Möglichkeit, dass Bilder in verschiedenen Größen generiert werden, um die Ladezeiten zu verkürzen. Im Hintergrund werden generierte Bildformate gecached um nur beim ersten Aufruf Rechenzeit zu brauchen.

Der Static-Flower (src/flower/static.ts) ist dafür zuständig, dass statische Dateien ausgeliefert werden. Anhand dessen ob die Adresse /media/ oder /static/ ist entscheidet er aus welchem Verzeichnis das jeweilige File geladen wird. Die statischen Dateien finden sich dabei im static/media oder static/files Verzeichnis.

Hauptsächlich wird dieses Feature genutzt um dynamische Bilder auszuliefern. Oder Datei-Uploads im System zu speichern.

Das Original-Bild:
https://styria.com/api/media/1668

Das Bild in 200 px Breite und Höhe im Seitenverhältnis:
https://styria.com/api/media/1668/200xauto

Websockets

Dafür kommt die Library ws zum Einsatz.

Websockets sind ab Version 2.5.0 integriert. Die Konfiguration der Websockets erfolgt über die Datei src/config/websockets.ts. Die Websockets sind dabei in den Flowern src/flowers/websockets.ts und src/flowers/websockets/ zu finden.

Benutzt wir dies nur bei der Votingplattform, um die Sperren für Einträge welche von den jeweiligen Redakteuren gerade verwendet werden, zu synchronisieren. Es war mal angedacht eine komplettes Management darum herum zu bauen(siehe auskommentierter Code unten), aber das wurde dann doch nicht umgesetzt.

// flower/flow.ts

this.server = http.createServer(this.flowHandler());

// WEBSOCKETS
// this.wsserver = new socketio.Server(this.server, {
//   cors: { origin: '*' },
// });
appspace.wsserver = new WebSocketServer({ server: this.server });

// for (const namespace of namespaces) {
//   logger.info(`Websocket: Adding namespace: ${namespace.name}`);
// const handler =
//   appspace.namespaces.set(namespace.name, handler);
// }

(appspace.wsserver as WebSocketServer).on('connection', (ws) => {
  logger.info('Flow v2: Websocket Connection');
  ws.on('message', LockNamespace.handle('message', ws));
  ws.on('error', LockNamespace.handle('error', ws));
  ws.on('close', LockNamespace.handle('close', ws));
});

Jobs

Jobs werden wie dem Bree Scheduler ausgeführt und sind in src/jobs zu finden. Sie können entweder einmalig oder in regelmäßigen Abständen ausgeführt werden. Die Jobs sind dabei in der src/config/jobs.ts konfiguriert.

https://github.com/breejs/bree

Mails

Mails können mithilfe einer Klasse zusammengestellt und versickt werden. Mails werden dafür über ein Builderpattern konfigurert. Zusätzlich gibt es noch die Möglichkeit Templates zu nutzen, die in src/mailtemplates zu finden sind.

Im Hintergrund bedient sich das System Nodemailer.

Konfiguriert wird der Nodemailer bis Version 2.2.0 über die Datei src/config/mail.ts. Ab Version 2.3.0 wird der Nodemailer über die Umgebungsvariable MAIL_TRANSPORTER konfiguriert.

<!-- mailtemplates/contact.de.html --->

<html xmlns="https://www.w3.org/1999/xhtml">
  <head>
    <title>Vielen Dank für ihre Anfrage</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0 " />
  </head>
  <body>
    <p>Anfrage styria.com - Kontaktformular</p>

    <p>
      Sprache: Deutsch<br />
      Vorname: %VORNAME%<br />
      Nachname: %NACHNAME%<br />
      Email: %EMAIL%<br />
    </p>
    <p>%TEXT%</p>
  </body>
</html>
const mail = new Email().to('mediagroup@styria.com').from('no-reply@styria.com');

if (locale == 'de') {
  mail.subject('Kontaktanfrage').fromTemplate('contact.de');
} else if (locale == 'en') {
  mail.subject('Contactrequest').fromTemplate('contact.en');
}

mail.replace('%TEXT%', body.text).replace('%VORNAME%', body.firstname).replace('%NACHNAME%', body.surname).replace('%EMAIL%', body.email);

await mail.send();

Datenbank-Skript

Hier ist ein kleines Script das dabei hilft die Standart-Datenbank für eine neue API anzulegen: