Studi Kasus #10: Aplikasi Wisata Indonesia – Autentikasi JWT untuk Admin


Studi Kasus #10: Aplikasi Wisata Indonesia – Autentikasi JWT untuk Admin

Halo, calon full-stack developer!

Sejauh ini, semua API yang kita buat bersifat publik. Artinya, siapa pun bisa mengakses, menambah, mengubah, atau menghapus data. Tentu ini tidak aman untuk aplikasi nyata. Kita perlu melindungi rute-rute admin (seperti CRUD kategori, destinasi, dll.) sehingga hanya pengguna terautentikasi yang bisa mengaksesnya.

Di Studi Kasus #10 ini, kita akan menambahkan sistem autentikasi menggunakan JWT (JSON Web Token). Fitur yang akan dibuat:

  • Registrasi admin baru.
  • Login (mendapatkan token).
  • Middleware untuk memverifikasi token.
  • Menerapkan middleware pada rute-rute yang memerlukan autentikasi.

Kita akan menggunakan bcryptjs untuk hashing password dan jsonwebtoken untuk membuat dan memverifikasi token. Mari kita mulai! 🚀

📁 Struktur Folder dan File

Kita akan menambahkan file baru di folder controllers, routes, dan middlewares:

backend/
├── controllers/
│   ├── authController.js             (baru)
│   └── ...
├── routes/
│   ├── authRoutes.js                 (baru)
│   └── ...
├── middlewares/
│   ├── authMiddleware.js             (baru)
│   └── upload.js
├── .env
└── ...

Instalasi Paket Baru

Jalankan perintah berikut di folder backend untuk menginstal paket yang dibutuhkan:

npm install bcryptjs jsonwebtoken
  • bcryptjs – untuk meng-hash password sebelum disimpan ke database.
  • jsonwebtoken – untuk membuat dan memverifikasi JWT.

Skema Tabel Users

Kita sudah memiliki tabel users yang dirancang di awal:

CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(50) NOT NULL UNIQUE,
  email VARCHAR(100) NOT NULL UNIQUE,
  password_hash VARCHAR(255) NOT NULL,
  role ENUM('admin', 'editor') DEFAULT 'editor',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Pastikan tabel ini sudah ada di database. Jika belum, buat dengan perintah di atas.

Membuat Auth Controller

Buat file controllers/authController.js dengan kode berikut:

const db = require('../config/db');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

// Registrasi user baru
const register = async (req, res) => {
  const { username, email, password, role } = req.body;

  // Validasi sederhana
  if (!username || !email || !password) {
    return res.status(400).json({ error: 'Username, email, dan password harus diisi' });
  }

  try {
    // Cek apakah username atau email sudah ada
    const [existing] = await db.query(
      'SELECT id FROM users WHERE username = ? OR email = ?',
      [username, email]
    );
    if (existing.length > 0) {
      return res.status(400).json({ error: 'Username atau email sudah digunakan' });
    }

    // Hash password
    const salt = await bcrypt.genSalt(10);
    const hashedPassword = await bcrypt.hash(password, salt);

    // Simpan ke database
    const [result] = await db.query(
      'INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, ?)',
      [username, email, hashedPassword, role || 'editor']
    );

    // Jangan kirim password hash ke response
    res.status(201).json({
      id: result.insertId,
      username,
      email,
      role: role || 'editor',
      message: 'Registrasi berhasil'
    });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Gagal registrasi' });
  }
};

// Login user
const login = async (req, res) => {
  const { email, password } = req.body;

  if (!email || !password) {
    return res.status(400).json({ error: 'Email dan password harus diisi' });
  }

  try {
    // Cari user berdasarkan email
    const [rows] = await db.query('SELECT * FROM users WHERE email = ?', [email]);
    if (rows.length === 0) {
      return res.status(401).json({ error: 'Email atau password salah' });
    }

    const user = rows[0];

    // Bandingkan password
    const isMatch = await bcrypt.compare(password, user.password_hash);
    if (!isMatch) {
      return res.status(401).json({ error: 'Email atau password salah' });
    }

    // Buat token JWT
    const payload = {
      id: user.id,
      username: user.username,
      email: user.email,
      role: user.role
    };

    const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1d' });

    res.json({
      message: 'Login berhasil',
      token,
      user: {
        id: user.id,
        username: user.username,
        email: user.email,
        role: user.role
      }
    });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Gagal login' });
  }
};

// Get current user (berdasarkan token)
const getCurrentUser = async (req, res) => {
  try {
    // req.user sudah di-set oleh middleware auth
    const [rows] = await db.query('SELECT id, username, email, role, created_at FROM users WHERE id = ?', [req.user.id]);
    if (rows.length === 0) {
      return res.status(404).json({ error: 'User tidak ditemukan' });
    }
    res.json(rows[0]);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Gagal mengambil data user' });
  }
};

module.exports = {
  register,
  login,
  getCurrentUser
};

Penjelasan:

  • register: Menerima username, email, password, dan role opsional. Password di-hash dengan bcrypt sebelum disimpan.
  • login: Mencari user berdasarkan email, membandingkan password, lalu menghasilkan JWT yang berisi payload user. Token dikirim ke client.
  • getCurrentUser: Mengembalikan data user berdasarkan token yang valid (digunakan untuk mengecek sesi).

Middleware Autentikasi

Buat file middlewares/authMiddleware.js untuk memverifikasi token yang dikirim client:

const jwt = require('jsonwebtoken');

const authMiddleware = (req, res, next) => {
  // Ambil token dari header Authorization
  const authHeader = req.header('Authorization');
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Akses ditolak. Token tidak ditemukan' });
  }

  const token = authHeader.replace('Bearer ', '');

  try {
    const verified = jwt.verify(token, process.env.JWT_SECRET);
    req.user = verified; // Simpan data user ke request
    next(); // Lanjut ke handler berikutnya
  } catch (err) {
    res.status(401).json({ error: 'Token tidak valid' });
  }
};

module.exports = authMiddleware;

Middleware ini akan mengecek keberadaan token di header Authorization: Bearer <token>. Jika valid, data user ditambahkan ke req.user dan request dilanjutkan. Jika tidak, kirim status 401.

Membuat Auth Routes

Buat file routes/authRoutes.js:

const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
const authMiddleware = require('../middlewares/authMiddleware');

// Register
router.post('/register', authController.register);

// Login
router.post('/login', authController.login);

// Get current user (perlu token)
router.get('/me', authMiddleware, authController.getCurrentUser);

module.exports = router;

Di app.js, daftarkan route ini:

const authRoutes = require('./routes/authRoutes');
app.use('/api/auth', authRoutes);

Menambahkan JWT Secret ke .env

Buka file .env dan tambahkan variabel baru:

JWT_SECRET=rahasia_super_aman_12345

Ganti dengan string acak yang panjang dan sulit ditebak. Jangan pernah commit file .env ke GitHub!

Melindungi Rute Admin dengan Middleware

Sekarang kita akan melindungi rute-rute yang hanya boleh diakses oleh admin (atau user terautentikasi). Misalnya, kita ingin semua rute CRUD untuk categories, destinations, dll. hanya bisa diakses jika user memiliki token valid.

Caranya, kita cukup menambahkan middleware authMiddleware sebelum handler di route. Contoh untuk categoryRoutes:

const express = require('express');
const router = express.Router();
const categoryController = require('../controllers/categoryController');
const authMiddleware = require('../middlewares/authMiddleware');

// Public routes (mungkin untuk membaca data, tapi kita lindungi semua)
// Jika ingin publik, jangan gunakan middleware
router.get('/', categoryController.getAllCategories); // Bisa publik
router.get('/:id', categoryController.getCategoryById); // Bisa publik

// Rute yang memerlukan autentikasi
router.post('/', authMiddleware, categoryController.createCategory);
router.put('/:id', authMiddleware, categoryController.updateCategory);
router.delete('/:id', authMiddleware, categoryController.deleteCategory);

module.exports = router;

Perhatikan: kita membiarkan GET tetap publik agar data bisa dibaca oleh pengunjung biasa. Rute POST, PUT, DELETE kita lindungi dengan authMiddleware. Sesuaikan dengan kebutuhan aplikasi.

Lakukan hal yang sama untuk routes destinations, accommodations, transportations, galleries, dan packages (kecuali untuk operasi baca yang publik).

Menguji API dengan Postman

Pastikan server berjalan (node app.js). Kita akan menguji endpoint autentikasi.

1. Register Admin

  • Method: POST
  • URL: http://localhost:3000/api/auth/register
  • Headers: Content-Type: application/json
  • Body (JSON):
    {
      "username": "admin",
      "email": "admin@example.com",
      "password": "rahasia123",
      "role": "admin"
    }
  • Klik Send. Respons sukses akan mengembalikan data user tanpa password.

2. Login

  • Method: POST
  • URL: http://localhost:3000/api/auth/login
  • Body (JSON):
    {
      "email": "admin@example.com",
      "password": "rahasia123"
    }
  • Klik Send. Respons akan berisi token JWT dan data user. Salin token untuk digunakan di langkah berikutnya.

3. Get Current User (dengan token)

  • Method: GET
  • URL: http://localhost:3000/api/auth/me
  • Headers: Authorization: Bearer <token_hasil_login>
  • Klik Send. Seharusnya mengembalikan data user yang sedang login.

4. Akses Rute yang Dilindungi (tanpa token)

  • Coba akses POST category tanpa token. Misal POST ke http://localhost:3000/api/categories dengan body JSON.
  • Seharusnya mendapat respons 401 Unauthorized.

5. Akses Rute yang Dilindungi (dengan token)

  • Tambahkan header Authorization: Bearer <token> pada request POST category.
  • Seharusnya berhasil menambah kategori (jika data valid).
💡 Tips: Di Postman, kamu bisa menyimpan token ke variabel environment agar mudah digunakan di request lain. Buat variable token di collection, lalu set nilainya dari respons login.

Penanganan Error

  • 400 Bad Request: Jika field wajib tidak diisi.
  • 401 Unauthorized: Jika token tidak ada, tidak valid, atau email/password salah.
  • 409 Conflict: Jika username atau email sudah terdaftar.
  • 500 Internal Server Error: Kesalahan server.

Langkah Selanjutnya

Dengan autentikasi JWT, backend kita kini aman. Di Studi Kasus #11, kita akan mulai membangun frontend React – inisialisasi proyek, setup router, dan menghubungkan ke backend. Kita akan membuat halaman publik yang menampilkan data dari API dan halaman admin yang dilindungi dengan login.

Pastikan semua endpoint autentikasi berfungsi dengan baik. Jika ada error, periksa kembali kode, terutama penggunaan JWT_SECRET dan bcrypt.

Sampai jumpa di tutorial berikutnya!


Ditulis dengan ❤️ untuk para pengembang yang ingin membangun aplikasi wisata.

Lebih baru Lebih lama

نموذج الاتصال