Server applicatie

De server applicatie bemiddelt tussen de frontend en de database. Wanneer de gebruiker een resource als een oefening wilt zien, opslaan, of aanpassen handelt de server dit verzoek af en doet dan een een verzoek naar de database om de gewenste operatie uit te voeren. Een applicatie als deze wordt vaak een web API genoemd. Het geeft op verzoek geen webpagina's terug maar beschrijvingen van resources als oefeningen en gebruikers.

Dit hoofdstuk behandelt de taal waarin de applicatie is geschreven, de architectuur, en authenticatie. Deze onderwerpen moeten een licht schijnen op de volgende problemen.

  • Hoe bouw ik de applicatie op zodat anderen het verder kunnen ontwikkelen?
  • Hoe maak ik een web interface voor de informatie opgeslagen in de database?

De aanpak was rapid prototyping en valideren met andere, serverside en frontend developers.

De documentatie van de server applicatie is de vinden in de bijlagen.

Python

De API is geschreven in Python. Dit is een general purpose programmeertaal, oftewel niet specifiek bedoeld voor web applicaties. Het is echter wel uitermate geschikt hiervoor door het volwassen aanbod van webframeworks. Websites als Reddit, Stripe, Instagram en Dropbox maken veel gebruik van de taal.

De taal staat bekend om een syntax zonder al te veel cryptische tekens en structuren en leest soms bijna als Engels.

for student in students:
    print student.name

Communicatie met de database

Een relationele database spreekt SQL, en de applicatie is geschreven in Python. Om communicatie te faciliteren is gebruik gemaakt van de SQLAlchemy library. Deze library abstraheert, of vertaalt de communicatie tussen relationele databases en Python zodat in de Python geen SQL geschreven hoeft te worden.

Een voorbeeld van het ophalen van een gebruiker in SQL.

SELECT * FROM user
WHERE user.username='Kareem'
LIMIT 1;

Dit wordt in Python:

User.query.filter_by(username='Kareem').first()

SQLAlchemy zorgt ook voor dat de input van een gebruiker dat we gebruiken bij het communiceren met de database eerst wordt opgeschoond. Wanneer dit niet gebeurd is er het risico op SQL Injection. Dat is de mogelijkheid voor een kwaadwillende gebruiker om in bijvoorbeeld een gebruikersnaam veld een extra aanvraag te schrijven als ); DROP TABLE exercises; --. Wanneer we de gebruikers input dan gebruiken om de gebruiker op te zijn we alle oefeningen kwijt.

Her daughter is named Help I'm trapped in a driver's license
factory.

bron: xkcd

Datamodel in SQLAlchemy

SQLAlchemy is naast een communicatie toolset ook een zogenaamde ORM (Object Relational Mapper). Een ORM kan Python objecten en attributen interpreteren als database tabellen en kolommen. Als we een User definiëren in Python:

class User(Base):  # Base is een object wat metadata bevat over de echte database
    __tablename__ = 'user'
    username = Column(String(32), nullable=False)
    password = Column(String(128), nullable=False)

Dan kunnen we in onze applicatie werken met Python objecten, en SQLAlchemy doet de vertaling naar rijen in een database tabel.

Uiteindelijk zijn alle tabellen en kolommen uit het vorige hoofdstuk op deze manier gedefinieerd in Python, inclusief de relaties ertussen. Nu kunnen we onze applicatie code schrijven zonder te wisselen tussen SQL en Python.

REST

REpresentational State Transfer is een architectuur stijl dat beschrijft hoe verschillende componenten binnen een systeem met elkaar communiceren om de staat van de applicatie te vertegenwoordigen en manipuleren (Fielding). Binnen de scope van dit project heeft dit invloed op hoe de server applicatie luistert naar, en antwoord geeft op verzoeken via het internet. De verzoeken zullen komen vanuit een browser waarin een javascript applicatie draait. De API en de client zijn dus twee gescheiden entiteiten.

De beschrijving van REST door Fielding heeft geleid tot de volgende design keuzes.

Het definiëren van een base URI

Een base URI een entree punt vanaf waar de API verder ontdekt kan worden. Wanneer een verzoek wordt gedaan op http://www.deapi.nl (dit is een voorbeeld url) ontvangt de client een antwoord met links waaruit het duidelijk wordt hoe verder te gaan.

{
    "login": "/v1/login", 
    "register": "/v1/users",
    "profile": "/v1/users/profile"
}

Elk verzoek staat op zichzelf

Dit betekent dat er tussen verzoeken door geen informatie over de verschillende verzoeken wordt onthouden. Een voorbeeld is de login flow. Wanneer een gebruiker inlogt stuurt de API slechts een geheime sleutel terug, de server onthoudt niet dat de client is ingelogt, het is aan de client om elk volgend verzoek te authenticeren met de sleutel. Het zorgt er ook voor dat als er iets fout gaat in één verzoek, dit geen effect heeft op volgende.

Semantisch correct gebruik van HTTP verbs

HTTP verbs (werkwoorden) geven een semantische waarde aan een verzoek.

Verb Operatie
GET Opvragen van resource(s) zonder het aan te passen
POST* Aanmaken van een nieuwe resource
PUT Aanpassen van een nieuwe resource. Vereist dat de complete representatie van de resource wordt meegestuurd, niet alleen de nieuwe waardes.
DELETE Verwijderen van een resource

* POST moet volgens de HTTP specificatie verwerkt worden volgens de semantics van de resource zelf. Dit bied wat flexibiliteit wanneer de resource ongebruikelijke operaties heeft. In een uitzondering kan het dus worden gebruikt voor andere soort operaties. (IETF)

In een vertegenwoordiging van een resource staan referenties naar gerelateerde resources. Zoals bijvoorbeeld waar de auteur van een oefening is te vinden.

"related": {
    "author": "/v1/users/8n5yvv",
    ...
}

Consistente URLs

Ten eerste is de huidige vorm van de API beschikbaar vanaf www.deapi.nl/v1. Dit betekend dat volgende iteraties beschikbaar kunnen zijn onder v2, v3 etc. Zo zullen updates aan de structuur niet alle frontends breken.

Ten tweede is elke soort resource beschikbaar onder een eigen top level uri zoals /v1/exercises, v1/questionnaires.

Veiligheid

Wachtwoorden mogen nooit in originele vorm opgeslagen worden in de database. De best practise is om deze te hashen. Een hash is een zodanig vervormde versie van de originele input dat het praktisch onmogelijk is om terug te draaien met het juiste algoritme. Het algoritme dat in deze applicatie gebruikt is heet BCrypt.

BCrypt

Omdat cryptografie een vak apart is en absoluut niet geïmplementeerd moet worden zonder expertise, gebruiken we hiervoor een bestaande implementatie

BCrypt zorgt er ook voor dat dezelfde wachtwoorden niet altijd dezelfde hash teruggeven. Dit wordt bereikt door bij het hashen een willekeurige waarde aan het wachtwoord te binden, de salt.

Bij het inloggen gebruikt BCrypt de bestaande hash als de salt om het gegeven wachtwoord te hashen. Als het resultaat overeenkomt is de gebruiker geauthenticeerd. Let dat we de gegeven wachtwoorden bij het registreren of inloggen nooit opslaan.

Zoals eerder besproken onthoud de API niet wie er is ingelogd. Maar we willen niet bij elk verzoek opnieuw een gebruikersnaam en wachtwoord invoeren. Hiervoor worden JSON Web Tokens gebruikt.

JSON Web Tokens (JWT)

JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted. (IETF)

Wachtwoorden worden gebruikt als initiële authenticatie van de gebruiker aan de API. De API zal op correcte gegevens antwoorden met een JWT.

{
    "access_token": "eyJhbGciOiJIUzI1NiIsImV4cCI6MTQ2NjE5MjUyNiwiaWF0IjoxNDYzNjAwNTI2fQ.eyJpZCI6Nzk0MzQxMTd9._ajPAhGUfsb2AesROUlqsnYBHh3rJv950Z5eUE5fgi4",
    "expires_in": 2592000,
    "token_type": "Bearer"
}

Deze token bevat informatie dat de gebruiker identificeert en is gesigned met een geheime sleutel die alleen de API kent. Deze token kan in de browser worden opgeslagen en zo zal elk verzoek, zonder het geven van een gebruikersnaam en wachtwoord toch geauthenticeerd worden. De implementatie van deze authenticatie wijze komt van een bedrijf Stormpath die hierin gespecialiseerd is het artikel "Secure Your REST API… The Right Way" heeft gepubliceerd.

oauth2 bron: stormpath

De implementatie

Flask

De beschreven architectuur is geïmplementeerd met behulp van een webframework die het makkelijk maakt om te communiceren over HTTP. Flask zorgt ervoor dat verzoeken via HTTP terecht komen bij het juiste stuk code.

@app.route('/exercises/<id>', methods=['GET'])
def get_exercise(id):
    # Haal de oefening met de gegeven uit op uit de database.
    # Of geef een 404 Not Found error terug.
    exercise = Exercise.query.get_or_404(id)

    # Geef een json representatie van de exercise terug.
    return jsonify(exercise.__dict__)

Dit is op zichzelf een kant en klare API die een oefening met de gewenste id ophaalt uit de database. In een meer complexe applicatie komt het neer op een meervoud van functies die in deze vorm zijn gekoppeld aan een url. Deze roepen op hun beurt andere onderdelen van de applicatie aan die gaan over het communiceren met de database, het valideren van gebruikers input en het formatteren van de aangevraagde informatie naar de juiste vorm.

Representatie van resources

De informatie uit de database moet in een fijne vorm worden teruggegeven. De client, de ontwikkelaar of de browser, moet kunnen zien welke attributen aangepast kunnen worden, waar gerelateerde resources te vinden zijn en mogelijk automatisch gegenereerde informatie zoals wanneer het als laatst is geüpdatet. Hiervoor zijn zogenaamde schema's gedefinieerd die de vorm beschrijven die de resource in JSON moet hebben.

Het basis schema is tot stand gekomen een aantal validatie rondes met Thomas Machielsen, Mik de Vries en oud collega's Elvirion Antersein en Kjeld Groot.

{
    "data": ...,
    "meta": ...,
    "related": ...
}

Waar data informatie zal bevatten welke aangepast kan worden of nodig is voor een nieuwe resource. Meta bevat alle automatisch gegenereerde informatie en related bevat alle gerelateerde resources. De specifieke content van deze velden hangt af van de resource. Wat volgt is een exercise.

{
    "data": {
        "title": "De beste concentratie oefening",
        "description": "Dit is een concentratie oefening.",
        "category": "concentratie",
        "difficulty": 0,
        "group_exercise": false,
        "private_exercise": false,
        "json": {},
        "duration": {
            "min": 0,
            "max": 5
        }
    },
    "related": {
        "author": "/v1/users/8n5yvv",
        "rating": "/v1/exercises/0jze04v/ratings"
    },
    "meta": {
        "created_at": "2016-05-12t08:48:15.941487+00:00",
        "edit_allowed": false,
        "favorited": false,
        "href": "/v1/exercises/0jze04v",
        "id": "0jze04v",
        "popularity": 4.0,
        "updated_at": "2016-05-12t08:48:15.941502+00:00",
        "average_rating": {
            "clear": 3,
            "effective": 4,
            "fun": 5,
            "rating": 4.16666666666667
        },
        "user_rating": {
            "clear": 4,
            "effective": 5,
            "fun": 5,
            "rating": 4.66666666666667
        }
    }
}

Één van de wensen van de oud collega's was dat sommige gerelateerde velden gelijk zichtbaar konden worden gemaakt in plaats van alleen een referentie ernaar. Dit is mogelijk gemaakt door in het verzoek de parameter bijvoorbeeld expand=author toe te voegen.

Paginering

Om de snelheid te bewaren worden voor grote collecties resources niet gelijk alles teruggegeven. De applicatie zal deze in pagina's verdelen, per default 10 items per pagina. Het resultaat bevat informatie over de rest van de pagina's. De gewenste pagina's en items per pagina is op te vragen middels de parameters page en per_page.

{
    "current": "/v1/exercises?per_page=1&page=1",
    "first": "/v1/exercises?per_page=1&page=1",
    "items": [
        ...
    ],
    "last": "/v1/exercises?per_page=10&page=11",
    "next": "/v1/exercises?per_page=10&page=2",
    "page": 1,
    "pages": 11,
    "per_page": 10,
    "prev": null,
    "total": 108
}

Sorteren, filteren en zoeken

De kern functionaliteiten eisen dat de oefeningen op basis van verschillende attributen opgehaald kunnen worden in verschillende volgordes. HTTP biedt hiervoor de meest simpele oplossing. Door parameters toe te voegen aan het verzoek, zoals al eerder is laten zien, kan de client specifieker aangeven welke resources er gewenst zijn.

De volgende parameters maken dit mogelijk:

  • category: De naam van een categorie.
  • order_by: Het attribuut waarop gesorteerd moet worden. Bijvoorbeeld popularity.
  • search: Zoektermen die middels de functionaliteit beschreven in het vorige hoofdstuk gebruikt worden om te zoeken in de titel en beschrijving van de oefeningen.
  • author: Een gebruikersnaam van wie je alle oefeningen wilt zien.

Bijvoorbeeld /v1/exercises?category=relaxatie&order_by=popularity&search="moodboard tekenen"

De applicatie zorgt er voor dat deze parameters mee worden genomen in het verzoek naar de database.

Het formatteren van oefeningen

Oefeningen worden uitgelegd middels een beschrijving van tekst. Maar voor een betere controle over de output is Markdown geïmplementeerd. Markdown biedt een simpele syntax om stijl en formatting toe te passen op tekst.

Bijvoorbeeld *italics* of **bold**. We kunnen ook
[links](http://www.google.com) toevoegen.
En plaatjes! ![sample](./example.png)
* En lijstjes

Bijvoorbeeld italics of bold. We kunnen ook links toevoegen. En plaatjes! sample

  • En lijstjes

Op de server wordt de content automatisch omgezet.

Natuurlijk is dit niet geschikt voor de doelgroep. Echter het biedt de mogelijkheid voor de frontend om dit te verstoppen achter een gebruikersvriendelijke toolbar.

Conclusie

Alle kern functionaliteiten zijn volgens de methodes in dit hoofdstuk geïmplementeerd door te bouwen op de fundamenten gelegd in de implementatie van het datamodel. Je kan oefeningen verzamelen, delen, beoordelen, en zoeken, filteren en sorteren op verschillende manieren.
Dit op een manier dat gevalideerd is door andere ontwikkelaars en zich probeert te houden aan bekende architecturen en best practices.

Voor de volgende iteraties is het zo dat de API in deze staat stabiel genoeg is om verschillende frontends te ondersteunen. Zonder dat anderen hoeven te sleutelen aan deze laag.