Lab 3: Koppla samman front end och back end

Vi ska i denna labb koppla ihop er front end med er back end. Ni ska även få implementera ett enklare autentiserings- och behörighetssystem. Med hjälp av autentisering ser vi till att endast inloggade användare får tillgång till datan vi vill skydda. Med behörighetssystemet ser vi till att olika användare har olika behörigheter, som ger dem olika rättigheter att komma åt och manipulera datan.

AJAX

AJAX möjliggör att hämta och skicka data även efter att en webbsida har laddats. Detta sker med hjälp av Javascript. Läs en introduktion här. Observera att exemplet som visas ej använder jQuery, och ser därför annurlunda ut än när ni använder jQuery för AJAX-anrop.

Förberedande frågor

Svara på följande frågor och skriv ner era svar innan demonstration 1. Vad står AJAX för? 2. Skissa fram en lösning för hur er server skulle kunna få reda på vilken användare som skickar en viss förfrågan.

Del 1: AJAX

Börja med att följa Påbörjande av ny labb.

Vi börjar att ändra i nuvarnade implementation på klient-sidan så att klienten hämtar data från er Flask-server (från labb 1) asynkront.

  1. Ersätt serverStub med AJAX-anrop i client.js och ta bort serverStub.js, testa om er nya implementation fungerar genom att ta bort filen serverStub.js och se ifall allt fortfarande fungerar.

    Tip

    Använd följande kodskiss för att ersätta anropet serverStub.getCars(). För att ersätta de andra metoderna kan nedanstående exempel modifieras med avseende på url och type. Er servers address bör sparas i en variabel för att enklare kunna ändras i framtiden.

    Notera att CORS gör skillnad på http://127.0.0.1 och http://localhost. Istället för att ha ett hårdkodat namn kan ni använda er av:

    host = window.location.protocol + '//' + location.host
    
    $.ajax({
       url: host + '/cars',
       type: 'GET', // eller 'PUT', 'POST' eller 'DELETE'
       // ...
       success: function(cars) {
          // cars innehåller nu den JSON-data som servern svarar med på /cars
       }
    

    Eftersom HTTP-metoden GET är så pass vanlig finns även en jQuery-metod, som är lite enklare att använda, tillgänglig:

    $.get(host + '/cars', function(cars) {
       // cars innehåller nu den JSON-data som servern svarar med på /cars
    }
    

    Observera att exemplen ovan gör samma sak. Fler metoder finns att hitta i dokumentationen.

    När ni skickar med JSON-data till exempel vid POST-anrop kan ni behöva sätta headern Content-Type till application/json.

Observera att alla anrop till servern ska vara asynkrona, d.v.s. async: false eller liknande metoder får ej användas.

Del 2: Användarmodellen

Vi ska bygga autentiseringssystem i Flask där vi sköter registrering, inloggning och behörighet till data.

Som ett första steg ska vi nu ändra lite i nuvarande implementation så det blir mer anpassat för att hantera olika typer av användare.

  1. Refaktorisera (döp om) er modell för Customer till User och lägg till ett nytt fält, is_admin. Fältet ska vara av typen boolean och med standardvärdet False.

    Det finns även andra lösningar för att skilja mellan olika typer av användare. I denna laboration är dock denna lösning praktisk eftersom den i princip endast kräver att vi lägger till ett fält i databasen.

    Som ni kan se har vi satt ett initialt värde för fältet is_admin, detta är enbart för att säkerställa att nya användare alltid sätts som icke-admin. Observera att ni även behöver uppdatera relationer och metoder på alla platser ni använt Customer.

    Tip

    Använd Ctrl+Shift+F i VSCode för att söka efter customer i hela mappen.

Del 3: Registrering och lösenordshantering på server

Nästa steg i vår autentiseringsprocess är att ge användaren möjlighet att identifiera sig genom att ange ett lösenord vid inloggning. En första idé för att lösa denna uppgift är att skapa en kolumn i vår databas för att lagra användarens lösenord som en sträng. Men nu behöver vi se upp, är det rimgligt att spara ett lösenord i plan text i en databas? Svaret är naturligtvis nej, nej och åter nej. Det är aldrig en bra idé att spara lösenord som plan text i en databas. Vi löser detta istället genom att använda en hash-funktion på lösenordet innan vi lagrar det i databasen.

I den här laborationen kommer vi fortsätta använda hjälpbibliotek precis som tidigare. I detta fall ska vi använda bcrypt vilken är ett hashnings-bibliotek. Läs igenom bcrypt dokumentationen för att hitta metoder som kan hjälpa oss att hasha lösenord innan de lagras i databasen.

När vi sedan ska validera huruvida en användare är autentisk kommer vi köra samma hash-funktion på den hävdade användarens angivna lösenord och jämföra med det hashade lösenordet vi lagrat i databasen.

  1. Installera flask-bcrypt i er utvecklingsmiljö och lägg in följande två rader på lämpliga platser i er main.py:

    from flask_bcrypt import Bcrypt
    
    bcrypt = Bcrypt(app)
    
  2. Lägg nu till ett fält i er användar-modell för att spara ett hashat lösenord. Döp det till password_hash och sätt typen till String. Sedan kan det vara smidigt att lägga till en metod i modellen som nedan för att enkelt kunna hasha och sätta lösenord.

    def set_password(self, password):
       self.password_hash = generate_password_hash(password).decode('utf8')
    
  3. Skapa nu funktionalitet för routen /sign-up.
    Ni ska ta emot JSON med en e-postadress, ett namn samt ett lösenord. Denna data ska användas för att spara en ny användare i databasen. Lösenordet ska hashas innan det sparas. Servern ska sedan svara med statuskoden 200.

    Tip

    Använd metoden set_password för att hasha lösenordet innan ni sparar det.

Del 4: Autentisering med token

Den övergripande strukturen vi kommer följa är token-baserad autentisering. Övergripande betyder det att token genereras på servern vid inloggning. Detta token används sedan vid alla requests för att avgöra vem användaren är (se nedanstående figur). Läs mer om token-baserad autentisering här.

Autensiering med bearer tokens

Eftersom vem som helst kan använda Postman eller andra verktyg för att skicka godtycklig request till en webbserver räcker det inte med att användaren måste logga in på klienten - servern måste alltid skyddas mot alla tänkbara förfrågningar.

För att skapa och läsa tokens ska vi använda oss av paketet Flask-Jwt-Extended. Detta paket kommer att benämnas som Flask-JWT framöver.

  1. Installera Flask-JWT genom att köra kommandot

    (venv) $ pip install flask-jwt-extended
    

    eller genom att använda er eventuella requirements.txt.

  2. Konfigurera Flask-JWT i main.py genom att:

    • Importera JWTManager från flask_jwt_extended

    • Sätt app.config['JWT_SECRET_KEY'] till någon svårgissad sträng

    • Initiera JWTManager: jwt = JWTManager(app)

  3. Skapa funktionalitet för POST /login. Metoden ska ta emot en e-post och ett lösenord. Kontrollera att de är korrekta och svara sedan med ett token och användardata (se till att inte skicka med lösenordshashen). Svaret ska alltså se ut såhär:

    _images/lab3-4-3.png

    Om inloggningsuppgifterna inte är korrekta ska servern svara med HTTP-statuskoden 401.

    Tip

    Använd create_access_token från Flask-JWT.

  4. Servern måste också hålla koll på att rätt användare får tillgång till rätt funktionalitet och data. Modifera er server-kod så att alla routes förutom /sign-up och /login kräver en autentiserad användare. Om en användare försöker komma åt en route och inte kan autentiseras, ska servern svara med statuskoden 401. Testa med Postman både med och utan auth-header.

    Tip

    Använd decoratorn @jwt_required som i dokumentationen

    I Postman kan ni skicka med ett token på detta vis:

    _images/lab3-4-4.png

Del 5: Registrering, inloggning och utloggning på klient

  1. Skapa registreringsmöjlihet:

    1. Skapa menyalternativet “Registrera dig”.

    2. Skapa en ny vy innehållande ett HTML-formulär som tar in e-post, namn och lösenord.

    3. Skapa funktionallitet för att skicka formulärets data till /sign-up så att en ny användare skapas.

    4. När användaren har skapats, se till att välkomstvyn visas istället för formuläret.

  2. Skapa inloggningsmöjlighet:

    1. Skapa menyalternativet “Logga in”.

    2. Skapa en ny vy innehållande ett HTML-formulär som tar in e-post och lösenord.

    3. Skapa funktionallitet för att skicka formulärets data till /login där uppgifterna ska valideras.

    4. Spara det token och den användardata som servern svarar med efter lyckad inloggning.

    Tip

    sessionStorage kan användas för att spara data i webbkläsaren på följande sätt:

    sessionStorage.setItem('auth', JSON.stringify(loginResponse))
    
    1. När användaren har lyckats logga in, se till att välkomstvyn visas.

    Tip

    Utvecklingsverktyget i webbläsaren kan användas för att läsa innehållet i sessionStorage. Det går även att ta bort värden med hjälp av detta. Utvecklingsverktygen i Chrome öppnas genom att trycka på F12-tangenten.

    _images/lab3-5-2-5.png
  3. Dölj menyalternativen “Logga in” och “Registrera dig” om det finns ett token sparat i webbläsaren. Se även till att dessa menyalternativ visas om ett token inte finns sparat.

    Tip

    Använd jQuery-metoden toggleClass() och Bootstrap-klassen d-none för att kontrollera synligheten för ett element. Denna logik kan med fördel placeras så att den körs när sidan laddas.

    var signedIn = sessionStorage.getItem('auth').length > 0;
    $(/*'selector för element att dölja'*/).toggleClass('d-none', !signedIn)
    
  4. Skapa utloggningsmöjlighet:

    1. Skapa menyalternativet Logga ut och visa denna enbart då en användare är inloggad.

    2. Ta bort token från sessionStorage då användaren loggat ut.

    Tip

    Använd sessionStorage.removeItem()

Del 6: Autentisering med AJAX

  1. Visa menyalternativet “Bilar” endast om användaren är inloggad.

  2. Skicka det token som ni sparade vid inloggning i varje request som ska autentiseras. (ej för /login och /sign-up). Det kan göras genom att skicka in följande argument till $.ajax, $.get och så vidare.

    headers: {"Authorization": "Bearer " + JSON.parse(sessionStorage.getItem('auth')).token}
    
  3. Visa knapparna “Redigera” och “Ta bort” på varje bil endast om användaren är admin.

  4. Skapa och visa en “Boka”-knapp på varje bil som visas om användaren inte är admin. Dock, om bilen redan är bokad, ska “Bokad av namn” visas istället för boka-knappen.

  5. Skapa POST /cars/[int:car_id]/booking som sätter en relation mellan den inloggade användaren och bilen ifall den inte redan är bokad. Servern ska svara med {success: true} om bokningen gick igenom och {success: false} om den redan var bokad av någon annan. Gör så att en förfrågan till denna route skickas när användaren klickar på en bils bokningsknapp och att texten “Bokad av namn” visas om bokningen lyckades.

    Tip

    Använd get_jwt_identity för att hitta den inloggade användaren.

  6. Skapa nu en lösning för att avboka bilar på motsvarande sätt som bilar bokas.

Redovisning

Se Redovisning.