Notes-demo 2 (2/2)
Käyttäjän lisääminen
Sovelluksestamme puuttuu vielä kirjautuminen ja rekisteröityminen. Ensimmäiseksi toteutetaan backendiin mahdollisuus luoda uusi käyttäjä. Uuden käyttäjän salasana salataan ja sitä varten tarvitaan kirjasto bcryptjs.
npm install bcryptjs --save
Ota bcryptjs käyttöön tiedostossa index.js
const bcrypt = require('bcryptjs')
Lisää backendin index.js-tiedostoon /register -toiminto:
app.post('/register', (req, res) => {
const user = req.body;
const saltRounds = 10;
console.log(user);
bcrypt.hash(user.password, saltRounds)
.then((passwordHash) => {
const newUser = {
username: user.username,
password: passwordHash,
email: user.email
}
knex('users').insert(newUser)
.then(() => {
console.log("register onnistui")
res.status(204).end()
})
})
})
Testaa käyttäjän lisäämistä REST-testillä (post_register.http):
POST http://localhost:3001/register HTTP/1.1
content-type: application/json
{
"username": "tester5",
"password": "salasana",
"email": "test@test.com"
}
Kirjautuminen
Kirjautuminen tarkistaa löytyykö tietokannasta käyttäjä ja vastaako salasana tietokannan salasanaan. Tämän jälkeen luodaan JSON Web Token, tähän käytetään jwt-kirjastoa. JWT allekirjoitetaan käyttäen salausavainta (SECRET). JWT tallennetaan selaimen muistiin ja se lähetetään aina jokaisen kutsun yhteydessä backendille. Käyttäjän id tallennetaan JWT:n sisälle, ei lähetetä puhtaana json-datana.
Asenna jwt-kirjasto
npm install jsonwebtoken --save
Ota jwt käyttöönn tiedostossa index.js
const jwt = require('jsonwebtoken')
Lisää /login-toiminto backendin index.js-tiedostoon:
app.post('/login', (req, res) => {
const user = req.body;
console.log(user);
knex('users').select('*').where('username', '=', user.username)
.then((dbuser) => {
if (dbuser.length == 0) {
return res.status(401).json(
{ error: "invalid username or password" }
)
}
const tempUser = dbuser[0];
bcrypt.compare(user.password, tempUser.password)
.then((passwordCorrect) => {
if (!passwordCorrect) {
return res.status(401).json(
{ error: "invalid username or password" }
)
}
//token
const userForToken = {
username: tempUser.username,
id: tempUser.id
}
const token = jwt.sign(userForToken, config.SECRET)
console.log(token);
res.status(200).send({
token,
username: tempUser.username
})
})
})
})
Testaa kirjautumista REST-testillä (post_login.http):
POST http://localhost:3001/login HTTP/1.1
content-type: application/json
{
"username": "tester1",
"password": "salasana"
}
Kirjautuminen palauttaa tokenin ja käyttäjänimen.
Frontend
Rekisteröityminen
Luo uusi tiedosto Register.jsx kansioon components. Laadi lomake jonka avulla käyttäjä voi rekisteröityä notesdemon käyttäjäksi.

Lisää funktio registerHandler tiedostolle App.jsx
const registerHandler = (e, userdata) => {
e.preventDefault();
console.log(userdata);
axios.post('http://localhost:3001/register', userdata)
.then(response => {
console.log(response.data)
setMessage("Rekisteröityminen onnistui")
})
}
Kirjautuminen
Tee seuraavaksi komponentti Login tiedostoon Login.jsx. Lisää samaan tapaan loginHandler-funktio tiedostolle App.jsx. URL-osoitteet voi määritellä muuttujiin loginURL ja registerURL.

const loginHandler = (e, userdata) => {
e.preventDefault();
axios.post(loginURL, userdata)
.then(response => {
console.log(response.data)
})
}
Kokeile kirjautumista ja tarkista tulostaako console.log saadun tokenin oikein.
Frontendin tulee säilyttää tieto käyttäjästä muistissa. Lisää uusi tilamuuttuja:
const [user, setUser] = useState(null);
Asetetaan user kirjautumisen then-haarassa:
setUser(response.data) //HUOM! tämä sisältää olion: {token, username}
Käyttäjän tulee pysyä kirjautuneena vaikka selainta päivitettäisiin. Tieto tokenista täytyy asettaa myös selaimen muistiin.
window.localStorage.setItem('notesdemouser', JSON.stringify(response.data))
Tarkistus ollaanko kirjauduttu vai ei tarvitsee oman hookin. Tehdään userHook joka tarkistaa löytyykö selaimen muistista notesdemouser.
const userHook = () => {
const loggedUserJSON = window.localStorage.getItem('notesdemouser')
if (loggedUserJSON) {
const user = JSON.parse(loggedUserJSON)
setUser(user)
}
}
Muuta vanha startHook ja lisää userHook
useEffect(userHook, []); useEffect(startHook, [user]);
startHook:in alussa tarkistetaan onko käyttäjä olemassa. Jos ei ole kirjauduttu ei haeta notes-tietoja
if (user === null) {
return;
}
Logout
Lisää painike uloskirjautumiselle App.jsx-tiedostoon. Painike kutsuu logoutHandler-funktiota joka tyhjentää notesdemouser-tiedon selaimesta.
const logoutHandler = () => {
setUser(null)
window.localStorage.removeItem('notesdemouser')
}
Käyttöliittymän ulkoasu
Nyt moni asia jo toimii mutta käyttöliittymä ei toimi ihan niin kuin pitäisi. Piilotetaan Notes ja NotesForm jos käyttäjä ei ole kirjautunut. Kun käyttäjä kirjautuu piilotetaan Login ja Register.
Kirjautumaton:
Kirjautunut:
Autentikaatio - backend
Viimeinen asia on autentikaatio, tarkistetaan milloin käyttäjä on kirjautunut. Backendissä tulee tietää kuka on kirjautunut: kenen muistiinpanot näytetään, kuka on käyttäjä joka luo uuden muistiinpanon tai muokkaa olemassa olevaa. Jokainen pyyntö sisältää tokenin joka sisältää käyttäjän id:n.
Apufunktio
Lisätään apufunktio joka lukee authorization-tiedon pyynnöstä. Funktio purkaa tokenin ja palauttaa sen.
const getTokenFrom = req => {
const authorization = req.get('authorization');
console.log(authorization);
if(authorization && authorization.toLowerCase().startsWith('bearer ')){
return authorization.substring(7)
} else {
return null
}
}
Muistiinpanojen hakeminen
Tee ensimmäisenä lisäys tokenin käsittelystä /GET -pyynnölle. Lisää seuraava koodi app.get-metodin alkuun. Luetaan token pyynnöstä ja annetaan virheilmoitus mikäli se puuttuu. Tämän jälkeen puretaan token käyttäen jwt.verify funktiota. Frontend lähettää tokenissa olion jonka sisällä on käyttäjän nimi ja id.
const token = getTokenFrom(req);
console.log(token);
if(!token){
return res.status(401).json(
{ error: "auth token missing" }
)
}
let decodedToken = null;
try{
decodedToken = jwt.verify(token, config.SECRET);
}
catch(error){
console.log("jwt error")
}
if(!decodedToken || !decodedToken.id){
return res.status(401).json(
{ error: "invalid token" }
)
}
Lisää REST-testi get_auth_notes.http jossa on mukana token (id:1). Varmista, että backend löytää käyttäjän muistiinpanot.
GET http://localhost:3001/notes HTTP/1.1 Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIxIiwiaWQiOjEsImlhdCI6MTYwMjE0NTY5OX0.lNsfJVKobqhpf8ZYU0-WopwWxwF1aPsQYjCD_U9c9Xo
Nyt tulee näkyviin vielä kaikki muistiinpanot koska knex.select ei rajoita tietoja. Lisää ehto mukaan:
where('user_id', '=', decodedToken.id)
Aja testi uudelleen ja varmista että nyt tulevat näkyviin vain yhden käyttäjän muistiinpanot.
Muut toiminnot
Kirjautuneen käyttäjän muita toimintoja ovat muistiinpanon lisääminen, muokkaaminen ja poistaminen. Lisää yllä oleva tokenin tarkistus myös näille toiminnoille. Muistiinpanon lisäämisessä korjaa nyt käyttäjän id:
note.user_id = decodedToken.id;
Autentikaatio - frontend
Backend toimii nyt oikein mutta frontend ei vielä lähetä tokenia. Kirjautuneen käyttäjän pyyntöihin tulee lisätä Authorization: bearer-teksti sekä token joka sisältää käyttäjän nimen ja id:n.
Lisää uusi kansio services ja sinne tiedostot notesService.js sekä userService.js. Siirretään notesService:n sisälle axios-kutsun tekeminen.
import axios from 'axios';
const baseUrl = 'http://localhost:3001/notes'
let token = null;
const setToken = newToken => {
token = newToken
}
const makeHeader = () => {
let header = {headers: {Authorization: `bearer ${token}`}}
return header;
}
const getAll = () => {
const request = axios.get(baseUrl, makeHeader())
return request.then(response => response.data)
}
const add = (newNote) => {
const request = axios.post(baseUrl, newNote, makeHeader())
return request.then(response => response.data)
}
const update = (id, updatedNote) => {
const request = axios.put(`${baseUrl}/${id}`, updatedNote, makeHeader())
return request.then(response => response.data)
}
const remove = (id) => {
return axios.delete(`${baseUrl}/${id}`, makeHeader())
}
export default {
getAll: getAll,
add: add,
update: update,
remove: remove,
setToken: setToken
}
Ota App.jsx-tiedostolla uusi notesService käyttöön:
import notesService from './services/notesService'
Poista tarpeeton baseURL ja muuta axios-kutsut käyttämään uutta notesService:a. Uusi tapa palauttaa suoraan response.data:n joten muuta App.jsx-tiedostolla response.data muotoon response. Esimerkkinä muistiinpanon lisääminen:
notesService.add(note).then(response => {
setMessage("Muistiinpano lisätty");
setNotes(notes.concat(response))
})
notesServicelle tulee kertoa token. Lisää loginHandler-funktiolle setUser-rivin viereen
notesService.setToken(response.data.token)
(kun olet lisännyt userServicen niin response.token)
userHook-funktiota kutsutaan selaimen päivittyessä, täälläkin tulee välittää tieto tokenista notesServicelle:
notesService.setToken(user.token)
userService
Lisää toiminnot login ja register tiedostolle userService. Tämä on yksinkertaisempi sillä emme tarvitse tietoa tokenista eikä siten ole tarvetta makeHeader-funktion kutsumiselle.
import axios from 'axios';
const registerURL = 'http://localhost:3001/register';
const loginURL = 'http://localhost:3001/login';
const login = (userdata) => {
const request = axios.post(loginURL, userdata)
return request.then(response => response.data)
}
const register = (userdata) => {
const request = axios.post(registerURL, userdata)
return request.then(response => response.data)
}
export default {
register: register,
login: login
}