Search by

    Terakhir diperbaharui: Jan 1, 2021

    Implementasi sistem autentikasi - Register

    Implementasi 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

    register workflow

    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');
    2
    3const url = process.env.MONGODB_URL;
    4const client = new MongoClient(url);
    5const dbName = 'dinotesDB';
    6
    7let db;
    8
    9exports.connect = (callback) => {
    10 client.connect((err, client) => {
    11 db = client.db(dbName);
    12 return callback(err, client);
    13 });
    14};
    15
    16exports.getDb = () => {
    17 return db;
    18};

    Update file server.js:

    1const express = require('express');
    2const bodyParser = require('body-parser');
    3const cors = require('cors');
    4
    5const dotenv = require('dotenv').config();
    6
    7const routes = require('./routes');
    8const handleErrors = require('./middlewares/errorHandler');
    9const { connect } = require('./utils/dbConnection');
    10const { logger } = require('./utils/logger');
    11
    12const app = express();
    13const port = process.env.PORT || 3001;
    14
    15app.use(cors());
    16
    17app.use(bodyParser.urlencoded({ extended: true }));
    18
    19app.use(bodyParser.json());
    20
    21// connect to database
    22connect((err, client) => {
    23 if (err) {
    24 console.log(err);
    25 }
    26});
    27
    28// Routes
    29app.use('/', routes);
    30
    31app.use(handleErrors);
    32
    33app.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');
    4
    5const { getDb } = require('./dbConnection');
    6
    7passport.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();
    17
    18 const existingUser = await db.collection('users').findOne({ username: email });
    19
    20 if (!existingUser) {
    21 const encryptedPassword = await bcrypt.hash(password, 10);
    22
    23 const data = {
    24 username: email,
    25 password: encryptedPassword,
    26 createdAt: new Date(Date.now()).toISOString(),
    27 updatedAt: new Date(Date.now()).toISOString()
    28 };
    29
    30 const user = await db.collection('users').insertOne(data);
    31
    32 return done(null, user);
    33 }
    34
    35 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: user
    8 });
    9 } else {
    10 res.status(200).json({
    11 message: 'Email already registered',
    12 });
    13 }
    14
    15 })(req, res, next);
    16};
    17...

    Pada file routes.js, tambahkan endpoint untuk menangani proses registrasi.

    routes.js

    1...
    2require('./utils/auth');
    3...
    4// user
    5router.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';
    5
    6import logo from '../assets/images/header-logo.png';
    7import Message from '../components/ui/Message';
    8
    9const 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`;
    18
    19const InfoWrapper = (props) => {
    20 const { status } = props;
    21
    22 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 };
    30
    31const RegisterPage = () => {
    32 const [isSuccess, setIsSuccess] = useState(null);
    33
    34 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 <Formik
    44 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 <EmailField
    61 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 <PasswordField
    69 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 Register
    79 </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};
    88
    89export 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';
    8
    9const Container = tw.div`text-center`;
    10
    11function 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}
    34
    35export 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';
    2
    3const initialState = {
    4 status: 'idle',
    5 error: null
    6};
    7
    8export 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 };
    14
    15 const response = await fetch(`${process.env.REACT_APP_API_URL}/register`, requestOptions);
    16
    17 if (response.ok) {
    18 return response;
    19 } else {
    20 throw Error(response.statusText);
    21 }
    22});
    23
    24const 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});
    45
    46export const { statusReset, logout } = userSlice.actions;
    47
    48export default userSlice.reducer;

    Daftarkan reducer ke store:

    src/store.js

    1import { configureStore } from '@reduxjs/toolkit';
    2
    3import notesReducer from './features/notes/notesSlice';
    4import userReducer from './features/user/userSlice';
    5
    6export default configureStore({
    7 reducer: {
    8 notes: notesReducer,
    9 user: userReducer
    10 }
    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...
    6
    7const RegisterPage = () => {
    8 const dispatch = useDispatch();
    9 const [isSuccess, setIsSuccess] = useState(null);
    10
    11 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 <Formik
    21 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 <EmailField
    52 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 <PasswordField
    60 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 Register
    70 </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: