Membuat Aplikasi Web Realtime dengan Django, RabbitMQ dan Vue.js Bagian 2: Otentikasi dan Manajemen User

Bagus Aji Santoso 30 Januari 2018

Membuat Aplikasi Web Realtime dengan Django, RabbitMQ dan Vue.js Bagian 2: Otentikasi dan Manajemen User

*Artikel ini merupakan terjemahan seri tutorial Real time Chat application built with Vue, Django, RabbitMQ and uWSGI WebSockets bagian kedua yang ditulis oleh Osaetin Daniel. *

Kita akan mulai pembuatan chatire dengan melakukan implementasi manajemen user dan otentikasi sehingga user bisa membuat akun baru dan melakukan login.

Berkat komunitas Django yang luar biasa, hal-hal tadi sudah tersedia untuk kita pakai. Oleh karena itu kita akan menggunakan pustaka django pihak ketiga bernama djoser

Mari kita pasang dari pypi

pip install djangorestframework
pip install djoser

Djoser adalah implementasi sistem otentikasi Django berbasis REST. Jadi dengan pustaka ini kita akan mendapatkan endpoint REST untuk melakukan registrasi user, pembuatan token, manajemen user dll.

Konfigurasi djoser

Kita mulai dengan konfigurasi palign sederhana untuk djoser. Tambahkan kode berikut di INSTALLED_APPS

INSTALLED_APPS = (
    'django.contrib.auth',
    ...,
    'rest_framework',
    'rest_framework.authtoken',
    'djoser',
)

lalu url djoser di urls.py:

from django.contrib import admin
from django.urls import path, include


urlpatterns = [
    ...,
    path('auth/', include('djoser.urls')),
    path('auth/', include('djoser.urls.authtoken')),
]

Masukkan rest_framework.authentication.TokenAuthentication ke dalam kelas otentikasi django rest:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.TokenAuthentication',
        (...)
    ),
}

Terakhir, jalankan migrasi database dengan perintah python manage.py migrate. Perintah ini akan membuat tabel-tabel yang dibutuhkan djsoer.

Hanya dengan menambah kode-kode di atas sekarang endpoint otentikasi sudah siap. Mari kita buat user baru (jalankan di terminal):

curl -X POST http://127.0.0.1:8000/auth/users/create/ --data 'username=danidee&password=mypassword'

{"email":"","username":"danidee","id":1}

Voila! Sekarang kita sudah memiliki use baru. Lihat dokumentasi djoser untuk semua endpoint yang tersedia dan bagaimana cara menggunakannya di http://djoser.readthedocs.io/en/latest/base_endpoints.html

Vue.js

Vue adalah Framework JavaScript untuk membuat antarmuka yang Reactive. Meskipun penulis merupakan fans berat React (karena React-native), penulis lebih memilih Vue untuk membuat aplikasi web.

Salah satu alasannya ialah kurva belajar yang lebih bersahabat. Ia lebih mudah untuk mulai dipelajari dan tidak seperti React dimana kita harus bersusah payah menyiapkan banyak hal (dengan Webpack dan kawan-kawannya) untuk membuat aplikasi yang production ready. Pembaca cukup menulis tag <script> seperti saat akan menggunakan JQuery.

Ia juga memiliki komunitas yang giat dengan banyak plugin dan tutorial yang tersedia.

Kita akan memakai vue-cli untuk membuat Vue app dengan cepat (tidak menggunakan tag <script>). Methode ini memungkinkan kita untuk menggunakan ES6+ dan single file Vue component.

Pasang vue-cli dari npm:

npm install -g vue-cli

Mari mulai proyek baru menggunakan webpack dengan vue-cli

vue init webpack chatire-frontend

**Catatan: Pastikan memilih opsi “install vue-router” **

Sekarang mungkin waktu yang tepat untuk membuat secangkir kopi atau mengambil snack karena prosesnya dapat memakan waktu lama tergantung kecepatan internet kita.

Selanjutnya, masuk ke direktori aplikasi vue yang sudah muncul (diterminal dengan perintah cd) dan jalankan server development dengan perintah:

npm run dev.

Seharusnya saat membuka alamat localhost:8080 di browser kita akan melihat:

Realtime Django 2.1

Mari kita bahas sebentar struktur foldernya:

.
├── build
│   ├── build.js
│   ├── check-versions.js
│   ├── logo.png
│   ├── utils.js
│   ├── vue-loader.conf.js
│   ├── webpack.base.conf.js
│   ├── webpack.dev.conf.js
│   └── webpack.prod.conf.js
├── config
│   ├── dev.env.js
│   ├── index.js
│   ├── prod.env.js
│   └── test.env.js
├── index.html
├── node_modules
├── package.json
├── package-lock.json
├── README.md
├── src
│   ├── App.vue
│   ├── assets
│   │   └── logo.png
│   ├── components
│   ├── main.js
│   └── router
│       └── index.js
├── static
└── test
    ├── e2e
    │   ├── custom-assertions
    │   │   └── elementCount.js
    │   ├── nightwatch.conf.js
    │   ├── runner.js
    │   └── specs
    │       └── test.js
    └── unit
        ├── jest.conf.js
        ├── setup.js
        └── specs
            └── HelloWorld.spec.js
  • build: Direktori ini berisi skrips yang dipakai untuk menjalankan server development webpack atau bundel aplikasi saat siap dipakai di production. Contoh, perintah npm run dev sebetulnya memanggil perintah:

    webpack-dev-server --inline --progress --config build/webpack.dev.conf.js

    Opsi --inline memasukkan file static yang di generate ke halaman index.html.

  • config: Sesuai namanya, disini tempat menyimpan file-file yang berhubungan dengan konfigurasi development, testing dan production

  • src: Disini adalah tempat menulis sebagian besar kode-kode yang kita perlukan, ia memiliki subfolder yang yang memiliki fungsi masing-masing.

    Component single file kita akan disimpan di folder ini. Akan ada satu component bawaan bernama HelloWorld.vue.

    File index.js di folder router memiliki konfigurasi untuk vue-router.

  • static: File static (HTML, CSS and JavaScript) disimpan di folder ini.

  • test: Terakhir, template webpack dari vue-cli akan mempermudah ini jika ingin melakukan pengujian aplikasi dengan membuat End to end tests (e2e) di atas Nightwatch dan unit tests yang berjalan dengan Jest.

    Pengujian yang ada dapat dijalankan dengan perintah npm run unit (untuk unit tests) dan npm run e2e untuk End to end tests.

vue-cli juga menyiapkan fitur hotreloading untuk kita untuk membantu kita sehingga tidak perlu melakukan refresh di browser.

Konfigurasi Vue router

Buat dua component, satu untuk halaman utama bernama Chat.vue dan satu lagi untuk User Authentication dan Signup bernama UserAuth.vue

Idealnya apa yang kita inginkan adalah menampilkan component berdasarkan status Login user. Jika User sudah melakukan Login, maka kita ingin menampilkan component Chat jika tidak kita ingin menampilkan component UserAuth.

Kita dapat melakukannya dengan menggunakan global navigation. Edit file router index.js agar memiliki kode sebagai berikut:

import Vue from 'vue'
import Router from 'vue-router'
import Chat from '@/components/Chat'
import UserAuth from '@/components/UserAuth'

Vue.use(Router)

const router = new Router({
  routes: [
    {
      path: '/chats',
      name: 'Chat',
      component: Chat
    },

    {
      path: '/auth',
      name: 'UserAuth',
      component: UserAuth
    }
  ]
})

router.beforeEach((to, from, next) => {
  if (sessionStorage.getItem('authToken') !== null || to.path === '/auth') {
    next()
  }
  } else {
    next('/auth')
  }
})

export default router

Method beforeEach dipanggil sebelum melakukan navigasi ke route manapun diaplikasi kita.

Jika sebuah token disimpan di dalam sessionStorage kita akan memperbolehkan navigation melanjutkan pekerjaannya dengan memanggil next() jika tidak kita kembali ke component auth.

Ke mana pun route yang dituju oleh user di aplikasi kita, fungsi ini akan memeriksa jika user memiliki auth token dan membawanya sesuai tujuan.

Halaman Login/Signup

Penulis sudah membuat halaman Login/Signup sederhana dengan Bootstrap 4. Berikut isi konten UserAuth.vue:

<template>
  <div class="container">
    <h1 class="text-center">Welcome to Chatire!</h1>
    <div id="auth-container" class="row">
      <div class="col-sm-4 offset-sm-4">
        <ul class="nav nav-tabs nav-justified" id="myTab" role="tablist">
          <li class="nav-item">
            <a class="nav-link active" id="signup-tab" data-toggle="tab" href="#signup" role="tab" aria-controls="signup" aria-selected="true">Sign Up</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" id="signin-tab" data-toggle="tab" href="#signin" role="tab" aria-controls="signin" aria-selected="false">Sign In</a>
          </li>
        </ul>

        <div class="tab-content" id="myTabContent">

          <div class="tab-pane fade show active" id="signup" role="tabpanel" aria-labelledby="signin-tab">
            <form @submit.prevent="signUp">
              <div class="form-group">
                <input v-model="email" type="email" class="form-control" id="email" placeholder="Email Address" required>
              </div>
              <div class="form-row">
                <div class="form-group col-md-6">
                  <input v-model="username" type="text" class="form-control" id="username" placeholder="Username" required>
                </div>
                <div class="form-group col-md-6">
                  <input v-model="password" type="password" class="form-control" id="password" placeholder="Password" required>
                </div>
              </div>
              <div class="form-group">
                <div class="form-check">
                  <input class="form-check-input" type="checkbox" id="toc" required>
                  <label class="form-check-label" for="gridCheck">
                    Accept terms and Conditions
                  </label>
                </div>
              </div>
              <button type="submit" class="btn btn-block btn-primary">Sign up</button>
            </form>
          </div>

          <div class="tab-pane fade" id="signin" role="tabpanel" aria-labelledby="signin-tab">
            <form @submit.prevent="signIn">
              <div class="form-group">
                <input v-model="username" type="text" class="form-control" id="username" placeholder="Username" required>
              </div>
              <div class="form-group">
                <input v-model="password" type="password" class="form-control" id="password" placeholder="Password" required>
              </div>
              <button type="submit" class="btn btn-block btn-primary">Sign in</button>
            </form>
          </div>
          
        </div>
      </div>
    </div>
  </div>
</template>

<script>
  const $ = window.jQuery // JQuery

  export default {

    data () {
      return {
        email: '', username: '', password: ''
      }
    }

  }
</script>

<style scoped>
  #auth-container {
    margin-top: 50px;
  }

  .tab-content {
    padding-top: 20px;
  }
</style>

Pada sisipan kode di atas, v-model dipakai untuk melakukan two way data binding untuk semua kolom input. Ini artinya apapun yang kita tulis di kolom tersebut dapat langsung diakses dari JavaScript menggunakan this.field_name.

Kita juga membuat event listeners di kedua form menggunakan @submit.prevent yang akan mendengarkan event form submitdari setiap form dan memanggil method yang diinginkan. Kita belum mengimplementasi method-method tersebut.

Karena kita menggunakan Bootstrap, daripada memanggil jQuery dari npm kita menggunakan variabel $ untuk mendaftarkan window.jQuery secara global.

Kita akan memakai method ajax jQuery untuk berkomunikasi dengan server. Silahkan gunakan pustaka ajax lain seperti Axios jika tidak ingin menggunakan jQuery. Pustaka ini cukup populer dikalangan pengguna Vue.

Jangan lupa untuk menambahkan include file-file CSS dan JavaScript Bootstrap di halaman index.html.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css" integrity="sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy" crossorigin="anonymous">

    <style>
      .nav-tabs .nav-item.show .nav-link, .nav-tabs .nav-link.active {
        outline: none;
      }
    </style>

    <title>chatire-frontend</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->

    <script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/js/bootstrap.min.js" integrity="sha384-a5N7Y/aK3qNeh15eJKGWxsqtnX/wWdSZSKp+81YjTmS15nvnvxKHuzaWwXHDli+4" crossorigin="anonymous"></script>
  </body>
</html>

Hasil yang didapat seharusnya:

Realtime Django 1.2

Cukup oke kan?

Mari Kita Ambil auth token dari Django

Ktia ingin mendaftarkan user, memasukkannya dan membawanya ke route Chat.

Untuk melakukannya, kita harus mengimplement method signUp dan signIn di kode sebelumnya:

methods: {
  signUp () {
    $.post('http://localhost:8000/auth/users/create/', this.$data, (data) => {
      alert("Your account has been created. You will be signed in automatically")
      this.signIn()
    })
    .fail((response) => {
      alert(response.responseText)
    })
  },

  signIn () {
    const credentials = {username: this.username, password: this.password}

    $.post('http://localhost:8000/auth/token/create/', credentials, (data) => {
      sessionStorage.setItem('authToken', data.auth_token)
      sessionStorage.setItem('username', this.username)
      this.$router.push('/chats')
    })
    .fail((response) => {
      alert(response.responseText)
    })
  }
}

Sekarang coba submit form tadi. Oops! Ada kesalahan:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:8000/auth/users/create. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).

CORS

Menurut situs Mozilla:

Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to let a user agent gain permission to access selected resources from a server on a different origin (domain) than the site currently in use. A user agent makes a cross-origin HTTP request when it requests a resource from a different domain, protocol, or port than the one from which the current document originated.

Secara sederhana CORS memperbolehkan rikues Ajax dari domain tertentu untuk melakukan XmlHttpRequest. Normalnya kita tidak bisa melakuakn XmlHttpRequest dari situs dengan domain yang berbeda (catatan: jika ada yang salah harap dikoreksi).

Dalam kasus ini, meskipun sama-sama berjalan di localhost, karena berjalan di port yang berbeda (8080 dan 8000) mereka dianggap berada di domain yang berbeda.

Domain yang memiliki skema (http atau https), hostname (localhost) dan port yang harus sama pula.

Jadi bagaimana cara menjalankan CORS di aplikasi django? Ada pustaka pihak ketiga untuk memudahkan hal tersebut bernama django-cors-headers.

pip install django-cors-headers

tambahkan di INSTALLED_APPS

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # Custom Apps
    'rest_framework',
    'rest_framework.authtoken',
    'corsheaders',
    'djoser'
]

tambahkan juga di (Pastikan ditulsi sebelum django.middleware.common.CommonMiddleware)

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',

    'corsheaders.middleware.CorsMiddleware',

    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

Terakhir atur CORS_ORIGIN_ALLOW_ALL = True Catat bahwa apa yang kita lakukan mengaktifkan CORS untuk semua domain. Ini boleh dilakukan untuk development tapi saat di production kita mungkin hanya ingin domain tertentu saja

CORS_ORIGIN_WHITELIST

Baca dokumentasi django-cors-header untuk melihat opsi lain.

Setelah melakukannya, kita sekarang bisa membuat akun dan melakukan login.

Dibelakang layar, django-cors-headers menggunakan sebuah Middleware untuk menambah header ke setiap rikues yang memberitahu Django bahwa rikues tersebut aman dan boleh dilakukan.

Logout

Karena kita memakai sessionStorage untuk menyimpan auth token, kita bisa memulai session baru dengan membuka tab baru.

Jika ingin menyimpan token di tab baru atau saat browser di restart kita bisa memakai localStorage. Ia memiliki API yang sama dengan sessionStorage sehingga kita cukup mengganti session ke local.

Lalu kita dapat membuat sebuah fugnsi yang mengapus token dari storage dengan memanggil removeItem. Berikut kode untuk melakukannya dengan localStorage.

localStorage.removeItem('authToken')

Kesimpulan

Tutorial kali ini cukup sampai manajemen user dan sistem otensikasi. Kita mulai dengan memasang djoser, sebuah pustaka django yang memberikan endpoint REST untuk otentikasi.

Kita juga menggunakan method ajax milik jQuery untuk memanggil endpoints tersebut.

Selanjutnya kita membahas Same origin policy sedikit dan belajar bagaimana memperbolehkan rikues Ajax dari aplikasi Vue ke backend Django menggunakan CORS lewat django-cors-headers.

Dibagian berikutnya, kita membuat model django dan API untuk aplikasi Chat.