.. _lab3: 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 :ref:`start_new_lab`. 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: .. code :: javascript host = window.location.protocol + '//' + location.host .. code :: javascript $.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: .. code :: javascript $.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: .. code :: python from flask_bcrypt import Bcrypt .. code :: python 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. .. code :: python 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 `__. .. figure:: images/lab3/lab3-4-1.png :alt: 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 .. code :: text (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: .. figure:: images/lab3/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: .. figure:: images/lab3/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: .. code :: javascript sessionStorage.setItem('auth', JSON.stringify(loginResponse)) 5. 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. .. figure:: images/lab3/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. .. code :: javascript 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. .. code :: javascript 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 :ref:`redovisning`.