Terakhir diperbaharui: Jan 1, 2021
Implementasi sistem autentikasi - Login
Selanjutnya membuat sistem login.
Alur autentikasi pada halaman Login
Step by step
- Konfigurasi PassportJS strategies untuk login user
- Menambah route untuk menangani login user
- Membuat halaman login
- Integrasi API
Konfigurasi strategi login
Tambahkan code berikut pada file auth.js.
dinotes-api/utils/auth.js
1...2passport.use(3 'login',4 new localStrategy(5 {6 usernameField: 'email',7 passwordField: 'password'8 },9 async (email, password, done) => {10 try {11 const db = getDb();12 const user = await db.collection('users').findOne({ username: email });1314 if (!user) {15 return done(null, false, { message: 'User not found' });16 }1718 const validate = await bcrypt.compare(password, user.password);1920 if (!validate) {21 return done(null, false, { message: 'Wrong Password' });22 }2324 return done(null, user, { message: 'Logged in Successfully' });25 } catch (error) {26 return done(error);27 }28 }29 )30);31...
Pada code di atas, yang dilakukan adalah:
- Cek data user yang login di dalam database
- Jika user ketemu, cocokan password dengan function
bcrypt.compare
- Jika username dan password yang dimasukan sudah sesuai eksekusi callback
return done(null, user, { message: 'Logged in Successfully' });
- Object user yang mewakili identitas user yang sudah divalidasi akan dikirimkan ke handler untuk router
Update handler & route
Tambahkan custom callback di handler:
handler.js
1...2exports.login = async (req, res, next) => {3 passport.authenticate('login', async (err, user, info) => {4 try {5 if (err || !user) {6 const error = new Error('An error occurred.');78 return next(error);9 }1011 req.login(user, { session: false }, async (error) => {12 if (error) return next(error);1314 const body = { _id: user._id, username: user.username };15 const token = jwt.sign(body, 'mys3cret');1617 return res.json({ user: body, token });18 });19 } catch (error) {20 return next(error);21 }22 })(req, res, next);23};
Ada yang perlu diperhatikan disini.
Proses login akan dilakukan dengan menambahkan JWT atau JSON Web Token, JWT ini berfungsi untuk menghasilkan token yang digunakan untuk proses authentikasi antara client dengan API.
JSON Web Token adalah standard yang digunakan untuk melakukan proses transimi data dalam bentuk JSON.
Baris code yang bertugas untuk menghasilkan token adalah:
1const body = { _id: user._id, username: user.username };2const token = jwt.sign(body, 'mys3cret');
Token yang dihasilkan akan dikirim ke client/user untuk selanjutnya disimpan di dalam localStorage atau session di browser.
string mys3cret adalah sandi yang digunakan untuk menghasilkan dan membongkar token, kita bisa menggantinya dengan yang lain.
Untuk bisa menggunakan JWT install package jsonwebtoken.
1yarn add jsonwebtoken
Import package:
1...2const jwt = require('jsonwebtoken');3...
Update file route.js dengan menambahkan endpoint untuk menangani proses login.
routes.js
1...2router.post('/login', login);3...
Buat halaman login
Langkah-langkah berikut ini dilakukan di dalam project dinotest-client dan bukan dinotes-api
Buat sebuah file baru Login.js di dalam folder pages, kemudian salin code berikut:
1import React, { useState } from 'react';2import tw from 'twin.macro';3import { Formik } from 'formik';45import logo from '../assets/images/header-logo.png';6import Message from '../components/ui/Message';78const Container = tw.div`min-h-screen flex items-center justify-center bg-gray-50 py-8 px-4 sm:px-6 lg:px-8`;9const Img = tw.img`w-auto m-auto sm:h-16`;10const FormContainer = tw.div`max-w-md w-full space-y-8`;11const Title = tw.h2`mt-6 text-center text-3xl font-extrabold text-gray-900`;12const LoginForm = tw.form`mt-8 space-y-6`;13const LoginFormGroup = tw.div`rounded-md shadow-sm -space-y-px`;14const EmailField = tw.input`appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm`;15const PasswordField = tw.input`appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm`;16const Button = tw.button`relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500`;1718const InfoWrapper = (props) => {19 const { status } = props;2021 if (status !== null) {22 if (status === false) {23 return <Message type="error" text="Something wrong" />;24 }25 return <Message type="success" text="Login successful!" />;26 }27 return <></>;28};2930const LoginPage = () => {31 const [isSuccess, setIsSuccess] = useState(null);3233 return (34 <Container>35 <FormContainer>36 <Img src={logo} alt="logo" />37 <p>38 <i>"Once upon a time, dinosaur use app to take a note"</i>39 </p>40 <Title>Login to DinoTes</Title>41 <InfoWrapper status={isSuccess} />42 <Formik43 initialValues={{ email: '', password: '' }}44 validate={(values) => {45 const errors = {};46 if (!values.email) {47 errors.email = 'Required';48 } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {49 errors.email = 'Invalid email address';50 }51 return errors;52 }}53 onSubmit={async (values, { setSubmitting }) => {54 setSubmitting(false);55 }}56 >57 {({ values, errors, touched, handleChange, handleSubmit, isSubmitting }) => (58 <LoginForm onSubmit={handleSubmit}>59 <LoginFormGroup>60 <EmailField61 type="email"62 name="email"63 placeholder="Email Address"64 onChange={handleChange}65 value={values.email}66 />67 {errors.email && touched.email && errors.email}68 <PasswordField69 type="password"70 name="password"71 placeholder="Password"72 onChange={handleChange}73 value={values.password}74 />75 {errors.password && touched.password && errors.password}76 </LoginFormGroup>77 <Button type="submit" disabled={isSubmitting}>78 Login79 </Button>80 <p>81 Don't have an account?{' '}82 <Link to="/register">83 <b>Register</b>84 </Link>85 </p>86 </LoginForm>87 )}88 </Formik>89 </FormContainer>90 </Container>91 );92};9394export default LoginPage;
Kemudian Update route di file App.js.
src/App.js
1import React from 'react';2import { Route, Switch } from 'react-router-dom';3import tw, { GlobalStyles } from 'twin.macro';4import HomePage from './pages/Home';5import AddPage from './pages/Add';6import EditPage from './pages/Edit';7import RegisterPage from './pages/Register';8import LoginPage from './pages/Login';910const Container = tw.div`text-center`;1112function App() {13 return (14 <div>15 <GlobalStyles />16 <Container>17 <Switch>18 <Route path="/register">19 <RegisterPage />20 </Route>21 <Route path="/login">22 <LoginPage />23 </Route>24 <Route path="/add">25 <AddPage />26 </Route>27 <Route path="/edit/:id">28 <EditPage />29 </Route>30 <Route path="/">31 <HomePage />32 </Route>33 </Switch>34 </Container>35 </div>36 );37}3839export default App;
Integrasi API
Update file userSlice.js:
1import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';23const userObj = JSON.parse(localStorage.getItem('user'));4const isLoggedIn = userObj ? true : false;56const initialState = {7 isLoggedIn: isLoggedIn,8 user: userObj?.user,9 status: 'idle',10 error: null11};1213export const register = createAsyncThunk('users/register', async (credential) => {14 const requestOptions = {15 method: 'POST',16 headers: { 'Content-Type': 'application/json' },17 body: JSON.stringify(credential)18 };1920 const response = await fetch(`${process.env.REACT_APP_API_URL}/register`, requestOptions);2122 if (response.ok) {23 return response;24 } else {25 throw Error(response.statusText);26 }27});2829export const login = createAsyncThunk('users/login', async (credential) => {30 const requestOptions = {31 method: 'POST',32 headers: { 'Content-Type': 'application/json' },33 body: JSON.stringify(credential)34 };3536 const response = await fetch(`${process.env.REACT_APP_API_URL}/login`, requestOptions);3738 if (response.ok) {39 const data = await response.json();40 localStorage.setItem('user', JSON.stringify(data));41 return data;42 } else {43 throw Error(response.statusText);44 }45});4647const userSlice = createSlice({48 name: 'user',49 initialState,50 reducers: {51 statusReset(state, action) {52 state.status = 'idle';53 }54 },55 extraReducers: {56 [login.pending]: (state, action) => {57 state.status = 'loading';58 },59 [login.fulfilled]: (state, action) => {60 state.status = 'succeeded';61 state.isLoggedIn = true;62 state.user = action.payload;63 },64 [login.rejected]: (state, action) => {65 state.status = 'failed';66 state.error = action.error.message;67 },68 [register.pending]: (state, action) => {69 state.status = 'loading';70 },71 [register.fulfilled]: (state, action) => {72 state.status = 'succeeded';73 },74 [register.rejected]: (state, action) => {75 state.status = 'failed';76 state.error = action.error.message;77 }78 }79});8081export const { statusReset } = userSlice.actions;8283export default userSlice.reducer;
Seperti yang disebutkan sebelumnya bahwa kita menyimpan token yang berasal dari server ke dalam localStorage.
Baris code yang bertugas untuk hal tersebut adalah:
1localStorage.setItem('user', JSON.stringify(data));
Praktek menyimpan JWT di dalam localStorage adalah praktek yang umum dilakukan dalam suatu proses autentikasi aplikasi.
Hal ini bertujuan agar user tidak kehilangan akses masuk ke aplikasi setiap kali browser direfresh, ditutup atau ketika koneksi ke server putus. Dan juga untuk memudahkan proses autentikasi pada API endpoint.
Kemudian kita tambahkan dua properties baru di dalam object initialState
yaitu isLoggedIn
dan user
.
isLoggedIn
, akan kita gunakan untuk memeriksa status login dari user dan juga akan digunakan untuk conditional rendering setiap halaman.
Value dari isLoggedIn
adalah true jika pada aplikasi ditemukan localStorage item bernama 'user'.
1const userObj = JSON.parse(localStorage.getItem('user'));2const isLoggedIn = userObj ? true : false;
Sedangkan user
berisi data _id dan username yang bisa dimanfaatkan untuk kebutuhan aplikasi di sisi client, seperti menampilkan profil dll.
Langkah selanjutnya adalah update halaman Login.js untuk menghubungkan antara reducer dengan view.
src/pages/Login.js
1...2import { Link, Redirect } from 'react-router-dom'3import { unwrapResult } from '@reduxjs/toolkit';4import { useDispatch, useSelector } from 'react-redux';5...6const LoginPage = () => {7 const dispatch = useDispatch();8 const isLoggedIn = useSelector((state) => state.user.isLoggedIn);9 const [isSuccess, setIsSuccess] = useState(null);1011 return (12 <>13 {isLoggedIn ? (14 <Redirect to="/" />15 ) : (16 <Container>17 <FormContainer>18 <Img src={logo} alt="logo" />19 <p>20 <i>"Once upon a time, dinosaur use app to take a note"</i>21 </p>22 <Title>Login to DinoTes</Title>23 <InfoWrapper status={isSuccess} />24 <Formik25 initialValues={{ email: '', password: '' }}26 validate={(values) => {27 const errors = {};28 if (!values.email) {29 errors.email = 'Required';30 } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {31 errors.email = 'Invalid email address';32 }33 return errors;34 }}35 onSubmit={async (values, { setSubmitting }) => {36 try {37 const actionResult = await dispatch(login(values));38 const result = unwrapResult(actionResult);39 if (result) {40 setIsSuccess(true);41 window.location.reload();42 } else {43 setIsSuccess(false);44 }45 } catch (err) {46 setIsSuccess(false);47 } finally {48 dispatch(statusReset());49 setSubmitting(false);50 }51 }}52 >53 {({ values, errors, touched, handleChange, handleSubmit, isSubmitting }) => (54 <LoginForm onSubmit={handleSubmit}>55 <LoginFormGroup>56 <EmailField57 type="email"58 name="email"59 placeholder="Email Address"60 onChange={handleChange}61 value={values.email}62 />63 {errors.email && touched.email && errors.email}64 <PasswordField65 type="password"66 name="password"67 placeholder="Password"68 onChange={handleChange}69 value={values.password}70 />71 {errors.password && touched.password && errors.password}72 </LoginFormGroup>73 <Button type="submit" disabled={isSubmitting}>74 Login75 </Button>76 <p>77 Don't have an account?{' '}78 <Link to="/register">79 <b>Register</b>80 </Link>81 </p>82 </LoginForm>83 )}84 </Formik>85 </FormContainer>86 </Container>87 )}88 </>89 );90};91...
Pada code di atas kita gunakan state isLoggedIn
yang diambil dari slice menggunakan hook useSelector
untuk melakukan conditional rendering, jika isLoggedIn
bernilai true maka kita akan arahakan user ke halaman utama dari aplikasi DinoTes.
Hasil Akhir: