Terakhir diperbaharui: Jan 1, 2021
Implementasi sistem autentikasi - Register
Daftar Isi
Persiapan Step by Step - Refactoring - Install PassportJS - Konfigurasi PassportJS - Update handler & route - Buat halaman Registrasi - Integrasi APIImplementasi authentikasi pada aplikasi DinoTes ini meliputi:
- Membuat sistem registrasi user baru
- Membuat sistem login
- Proteksi API endpoint
- Menambah fungsi logout
Alur autentikasi pada halaman Register
- User melakukan proses registrasi menggunakan username & password
- DinoTes menyimpan credential user di dalam database dalam kondisi terenkripsi
- User dapat menggunakan credential yang baru dibuat untuk login dan menggunakan aplikasi
Persiapan
Clone repository aplikasi dinotes-api dan dinotes-client disini.
Step by step
Secara garis besar langkah-langkah yang akan kita lakukan adalah:
- Refactoring, membuat collection baru untuk menyimpan data user pada database MongoDB
- Install package PassportJS
- Konfigurasi PassportJS strategies di bagian API/server untuk kebutuhan registrasi user
- Menambah route untuk menangani registrasi user
- Membuat halaman registrasi
- Integrasi API
Refactoring
Buat sebuah collection baru di MongoDB dengan nama users untuk menyimpan data user.
Kita bisa membuat collection baru menggunakan MongoDB Compass, MongoDB Shell atau menambahkannya secara programmatically dengan MongoClient.
Sebelum melanjutkan ada yang perlu di refactoring.
Karena aplikasi dinotes dipersiapkan untuk digunakan oleh banyak user, menyimpan semua data notes di dalam object app.locals bukanlah ide yang bagus.
Kita perlu memisahkan baris code yang berfungsi menghandle autentikasi ke file tersendiri.
Pada project dinotes-api buat sebuah file di dalam folder utils dengan nama dbConnection.js, kemudian pindah baris code yang berfungsi untuk autentikasi dari file server.js:
1const { MongoClient } = require('mongodb');23const url = process.env.MONGODB_URL;4const client = new MongoClient(url);5const dbName = 'dinotesDB';67let db;89exports.connect = (callback) => {10 client.connect((err, client) => {11 db = client.db(dbName);12 return callback(err, client);13 });14};1516exports.getDb = () => {17 return db;18};
Update file server.js:
1const express = require('express');2const bodyParser = require('body-parser');3const cors = require('cors');45const dotenv = require('dotenv').config();67const routes = require('./routes');8const handleErrors = require('./middlewares/errorHandler');9const { connect } = require('./utils/dbConnection');10const { logger } = require('./utils/logger');1112const app = express();13const port = process.env.PORT || 3001;1415app.use(cors());1617app.use(bodyParser.urlencoded({ extended: true }));1819app.use(bodyParser.json());2021// connect to database22connect((err, client) => {23 if (err) {24 console.log(err);25 }26});2728// Routes29app.use('/', routes);3031app.use(handleErrors);3233app.listen(port, () => {34 logger.info(`Server listening at http://localhost:${port}`);35});
Karena sudah tidak lagi menggunakan app.locals kita perlu update semua handler (handler.js):
Ganti baris code berikut ini:
1const { notesCollection } = req.app.locals;
Dengan:
1const db = getDb();
Update semua variable notesCollection
dengan db.collection('notes')
.
Jangan lupa untuk import function getDb()
dari dbConnection.js.
1const { getDb } = require('./utils/dbConnection');
Install PassportJS
Pada project dinotes-api, buka integrated terminal kemudian jalankan perintah berikut:
1yarn add passport
Konfigurasi PassportJS
Minimal ada 2 hal yang perlu dikonfigurasi, yaitu strategies dan middlewares.
Strategies atau strategy adalah cara PassportJS melakukan proses autentikasi.
Ada lebih dari 500 strategies yang bisa digunakan, cara menggunakan strategies ini adalah dengan menginstall package atau modul.
Karena kita akan menggunakan local authentication/password-based maka kita perlu install package passport-local.
Install package passport-local menggunakan terminal:
1yarn add passport-local
Kita juga akan menggunakan package tambahan bernama bcrypt yang berfungsi untuk enkripsi password yang akan disimpan di dalam database.
1yarn add bcrypt
Semua code yang berkaitan dengan autentikasi akan kita simpan di dalam file dengan nama auth.js di dalam folder utils.
Buat sebuah PassportJS strategy untuk menghandle registrasi.
utils/auth.js
1const passport = require('passport');2const localStrategy = require('passport-local').Strategy;3const bcrypt = require('bcrypt');45const { getDb } = require('./dbConnection');67passport.use(8 'register',9 new localStrategy(10 {11 usernameField: 'email',12 passwordField: 'password'13 },14 async (email, password, done) => {15 try {16 const db = getDb();1718 const existingUser = await db.collection('users').findOne({ username: email });1920 if (!existingUser) {21 const encryptedPassword = await bcrypt.hash(password, 10);2223 const data = {24 username: email,25 password: encryptedPassword,26 createdAt: new Date(Date.now()).toISOString(),27 updatedAt: new Date(Date.now()).toISOString()28 };2930 const user = await db.collection('users').insertOne(data);3132 return done(null, user);33 }3435 return done(null, false, { message: 'User already registered' });36 } catch (error) {37 done(error);38 }39 }40 )41);
Pada code di atas, yang dilakukan adalah:
- Cek data existing user di dalam database dengan mencocokan username
- Jika belum ada user yang teregistrasi dengan username tersebut maka enkripsi password dengan menggunakan bcrypt
- Buat sebuah object berisi username (email), password yang terenkripsi dan waktu registrasi (createdAt & updatedAt)
- Insert object ke dalam database
- Eksekusi callback
return done(null, user)
, yang berfungsi untuk mengirimkan data user yang sudah divalidasi ke passportJS untuk selanjutnya diteruskan ke handler di dalam router expressJS
Update handler & route
Langkah selanjutnya adalah menambah custom callback sebagai handler untuk menangani proses registrasi di dalam handler.js.
handler.js
1...2exports.register = async (req, res, next) => {3 passport.authenticate('register', { session: false }, async (err, user, info) => {4 if(user) {5 res.status(200).json({6 message: 'Register Successful',7 user: user8 });9 } else {10 res.status(200).json({11 message: 'Email already registered',12 });13 }1415 })(req, res, next);16};17...
Pada file routes.js, tambahkan endpoint untuk menangani proses registrasi.
routes.js
1...2require('./utils/auth');3...4// user5router.post('/register', register);6...
Buat halaman Registrasi
Langkah-langkah berikut ini dilakukan di dalam project dinotest-client dan bukan dinotes-api
Buat sebuah Page baru dengan nama Register.js kemudian salin code berikut ini:
src/pages/Register.js
1import { Link } from 'react-router-dom';2import tw from 'twin.macro';3import { Formik } from 'formik';4import { unwrapResult } from '@reduxjs/toolkit';56import logo from '../assets/images/header-logo.png';7import Message from '../components/ui/Message';89const Container = tw.div`min-h-screen flex items-center justify-center bg-gray-50 py-8 px-4 sm:px-6 lg:px-8`;10const Img = tw.img`w-auto m-auto sm:h-16`;11const FormContainer = tw.div`max-w-md w-full space-y-8`;12const Title = tw.h2`mt-6 text-center text-3xl font-extrabold text-gray-900`;13const RegisterForm = tw.form`mt-8 space-y-6`;14const RegisterFormGroup = tw.div`rounded-md shadow-sm -space-y-px`;15const 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`;16const 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`;17const 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`;1819const InfoWrapper = (props) => {20 const { status } = props;2122 if (status !== null) {23 if (status === false) {24 return <Message type='error' text='Something wrong' />;25 }26 return <Message type='success' text="Your account has been created, please login " />;27 }28 return <></>;29 };3031const RegisterPage = () => {32 const [isSuccess, setIsSuccess] = useState(null);3334 return (35 <Container>36 <FormContainer>37 <Img src={logo} alt="logo" />38 <p>39 <i>"Once upon a time, dinosaur use app to take a note"</i>40 </p>41 <Title>Create your account</Title>42 <InfoWrapper status={isSuccess} />43 <Formik44 initialValues={{ email: '', password: '' }}45 validate={(values) => {46 const errors = {};47 if (!values.email) {48 errors.email = 'Required';49 } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {50 errors.email = 'Invalid email address';51 }52 return errors;53 }}54 onSubmit={async (values, { setSubmitting }) => {55 setSubmitting(false);56 >57 {({ values, errors, touched, handleChange, handleSubmit, isSubmitting }) => (58 <RegisterForm onSubmit={handleSubmit}>59 <RegisterFormGroup>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 </RegisterFormGroup>77 <Button type="submit" disabled={isSubmitting}>78 Register79 </Button>80 <p>Already have account? <Link to='/login'><b>Login</b></Link></p>81 </RegisterForm>82 )}83 </Formik>84 </FormContainer>85 </Container>86 );87};8889export default RegisterPage;
Perlu diperhatikan bahwa kita menambahkan package bernama Formik untuk mempermudah kita dalam membuat React Form, penambahan package ini dilakukan agar validasi input email & password lebih mudah dilakukan.
Install Formik:
1yarn add formik
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';89const Container = tw.div`text-center`;1011function App() {12 return (13 <div>14 <GlobalStyles />15 <Container>16 <Switch>17 <Route path="/register">18 <RegisterPage />19 </Route>20 <Route path="/add">21 <AddPage />22 </Route>23 <Route path="/edit/:id">24 <EditPage />25 </Route>26 <Route path="/">27 <HomePage />28 </Route>29 </Switch>30 </Container>31 </div>32 );33}3435export default App;
Integrasi API
Agar halaman registrasi dapat digunakan kita harus integrasikan antara halaman Registrasi dengan API endpoint /register
.
Buat sebuah slice baru untuk mengelola state yang berhubungan dengan user.
Buat file dengan nama userSlice.js di dalam folder features dan subfolder user.
Salin code berikut ini:
src/features/user/userSlice.js
1import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';23const initialState = {4 status: 'idle',5 error: null6};78export const register = createAsyncThunk('users/register', async (credential) => {9 const requestOptions = {10 method: 'POST',11 headers: { 'Content-Type': 'application/json' },12 body: JSON.stringify(credential)13 };1415 const response = await fetch(`${process.env.REACT_APP_API_URL}/register`, requestOptions);1617 if (response.ok) {18 return response;19 } else {20 throw Error(response.statusText);21 }22});2324const userSlice = createSlice({25 name: 'user',26 initialState,27 reducers: {28 statusReset(state, action) {29 state.status = 'idle';30 },31 },32 extraReducers: {33 [register.pending]: (state, action) => {34 state.status = 'loading';35 },36 [register.fulfilled]: (state, action) => {37 state.status = 'succeeded';38 },39 [register.rejected]: (state, action) => {40 state.status = 'failed';41 state.error = action.error.message;42 }43 }44});4546export const { statusReset, logout } = userSlice.actions;4748export default userSlice.reducer;
Daftarkan reducer ke store:
src/store.js
1import { configureStore } from '@reduxjs/toolkit';23import notesReducer from './features/notes/notesSlice';4import userReducer from './features/user/userSlice';56export default configureStore({7 reducer: {8 notes: notesReducer,9 user: userReducer10 }11});
Update halaman Registrasi.js agar reducer pada userSlice dapat dieksekusi via dispatch.
1...2import { unwrapResult } from '@reduxjs/toolkit';3import { useDispatch } from 'react-redux';4import { register, statusReset } from '../features/user/userSlice';5...67const RegisterPage = () => {8 const dispatch = useDispatch();9 const [isSuccess, setIsSuccess] = useState(null);1011 return (12 <Container>13 <FormContainer>14 <Img src={logo} alt="logo" />15 <p>16 <i>"Once upon a time, dinosaur use app to take a note"</i>17 </p>18 <Title>Create your account</Title>19 <InfoWrapper status={isSuccess} />20 <Formik21 initialValues={{ email: '', password: '' }}22 validate={(values) => {23 const errors = {};24 if (!values.email) {25 errors.email = 'Required';26 } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {27 errors.email = 'Invalid email address';28 }29 return errors;30 }}31 onSubmit={async (values, { setSubmitting }) => {32 try {33 const actionResult = await dispatch(register(values));34 const result = unwrapResult(actionResult);35 if (result) {36 setIsSuccess(true);37 } else {38 setIsSuccess(false);39 }40 } catch (err) {41 setIsSuccess(false);42 } finally {43 dispatch(statusReset());44 setSubmitting(false);45 }46 }}47 >48 {({ values, errors, touched, handleChange, handleSubmit, isSubmitting }) => (49 <RegisterForm onSubmit={handleSubmit}>50 <RegisterFormGroup>51 <EmailField52 type="email"53 name="email"54 placeholder="Email Address"55 onChange={handleChange}56 value={values.email}57 />58 {errors.email && touched.email && errors.email}59 <PasswordField60 type="password"61 name="password"62 placeholder="Password"63 onChange={handleChange}64 value={values.password}65 />66 {errors.password && touched.password && errors.password}67 </RegisterFormGroup>68 <Button type="submit" disabled={isSubmitting}>69 Register70 </Button>71 <p>Already have account? <Link to='/login'><b>Login</b></Link></p>72 </RegisterForm>73 )}74 </Formik>75 </FormContainer>76 </Container>77 );78};79...
Hasil akhir: