Express Js

Implementasi Autentikasi dan Relasi Database pada MongoDB dengan Mongoose

Gunakan project movie yang telah kalian buat pada pertemuan sebelumnya

Author : Celvine Adi Putra | Date : December 6, 2025

Pendahuluan

Tutorial ini berfokus pada pengembangan fitur keamanan dan struktur data pada aplikasi Movie Express. Dua konsep dasar yang akan diimplementasikan adalah:

  1. Authentication (Otentikasi): Mekanisme verifikasi identitas pengguna menggunakan JSON Web Token (JWT).
  2. Database Relations (Relasi Database): Menghubungkan data antar koleksi (collections) MongoDB, spesifiknya antara MovieModel dan UserModel.

Tujuan akhir dari implementasi ini adalah membuat API yang aman di mana setiap data film memiliki asosiasi kepemilikan dengan pengguna yang membuatnya, serta membatasi akses modifikasi data hanya kepada pemilik data tersebut.

Prasyarat (Prerequisites)

Pastikan pada laptop / PC nya sudah terinstall:

  • Project "Movie Express": Kode sumber dari tahap sebelumnya tersedia.
  • MongoDB Service: Layanan database MongoDB aktif (running).
  • Postman: Terinstal untuk keperluan pengujian endpoint API.

Langkah 1: Install Dependencies

Sistem otentikasi membutuhkan pustaka tambahan untuk menangani enkripsi password dan pembuatan token.

Terminal / CMD

Jalankan perintah berikut pada direktori root project movie:

npm install jsonwebtoken
npm install bcrypt

Penjelasan

  • jsonwebtoken: Pustaka untuk men-generate dan memverifikasi token. Memungkinkan penerapan stateless authentication.
  • bcrypt: Pustaka untuk melakukan hashing password. Password akan disimpan dalam bentuk terenkripsi (bukan plain text) untuk keamanan data pengguna jika terjadi kebocoran database.

Langkah 2: Membuat User Model

Langkah ini mendefinisikan struktur data pengguna dalam database.

Buat file baru: models/userModel.js.

models/userModel.js
import mongoose from "mongoose";

const UserSchema = new mongoose.Schema({
    username: {
        type: String,
        unique: true,
        required: true,
        trim: true
    },
    email: {
        type: String,
        unique: true,
        required: true,
        trim: true
    },
    password: {
        type: String,
        required: true
    }
}, {
    timestamps: true
})

const UserModel = mongoose.model("users", UserSchema)

export default UserModel;

Penjelasan Kode

  • unique: true: Mencegah duplikasi data pada username dan email.
  • trim: true: Menghapus spasi berlebih di awal dan akhir input string.
  • timestamps: Secara otomatis membuat field createdAt dan updatedAt.

Langkah 3: Membuat Utils

Modul utilitas diperlukan untuk memisahkan logika enkripsi dan tokenisasi dari business logic utama.

Buat folder utils, kemudian buat dua file di dalamnya: hashUtil.js dan jwtUtil.js.

1. Hash Utility

utils/hashUtil.js
import bcrypt from "bcrypt";

export const hashedPassword = async (password) => {
    return await bcrypt.hash(password, 12);
}

export const verifyPassword = async (password, hashedPassword) => {
    return await bcrypt.compare(password, hashedPassword);
}

2. JWT Utility

utils/jwtUtil.js
import jwt from 'jsonwebtoken'

export const getJwtToken = (user_id, username) => {
    const payload = {
        user_id: user_id,
        username: username
    }

    return jwt.sign(payload, "APP_JWT_SECRET", {
        expiresIn: '15m' // Token berlaku selama 15 menit
    })
}

Catatan: String APP_JWT_SECRET boleh di ganti tetapi harus konsisten (jika tidak menggunakan .env)


Langkah 4: User Controller

Controller ini menangani logika pendaftaran (Sign Up) dan masuk (Sign In).

Buat file baru: controllers/userController.js.

controllers/userController.js
import UserModel from "../models/userModel.js";
import { hashedPassword, verifyPassword } from "../utils/hashUtil.js";
import { getJwtToken } from "../utils/jwtUtil.js";

export const signIn = async (req, res) => {
    try {
        const { email, password } = req.body;

        if (!email || !password) {
            return res.status(400).send({
                error: 'Email dan password wajib diisi',
                data: null
            });
        }

        // Mencari user berdasarkan email
        const user = await UserModel.findOne({ email });
        if (!user) {
            return res.status(400).send({
                error: 'Email atau password salah',
                data: null,
            });
        }

        // Verifikasi password
        const isMatch = await verifyPassword(password, user.password);
        if (!isMatch) {
            return res.status(400).send({
                error: 'Password salah',
                data: null
            });
        }

        // Generate token jika valid
        const token = getJwtToken(user._id, user.username);

        return res.status(200).send({
            message: 'Login berhasil',
            data: { token }
        });
    } catch (error) {
        return res.status(400).send({
            message: error.message,
            error,
            data: null
        });
    }
};

export const signUp = async (req, res) => {
    try {
        const { username, email, password } = req.body;

        if (!username || !email || !password) {
            return res.status(400).send({
                error: 'Username, email, dan password wajib diisi',
                data: null,
            });
        }

        // Enkripsi password sebelum disimpan
        const hashPassword = await hashedPassword(password);

        const newUser = await UserModel.create({
            username,
            email,
            password: hashPassword,
        });

        if (newUser) {
            return res.status(200).send({
                message: 'Berhasil melakukan pendaftaran, silakan login',
                data: null,
            });
        }

        return res.status(500).send({
            message: 'Gagal melakukan pendaftaran, silakan coba lagi',
            data: null,
        });
    } catch (error) {
        return res.status(400).send({
            message: error.message,
            error,
            data: null,
        });
    }
};

Langkah 5: Middleware Otentikasi

Middleware berfungsi sebagai gerbang validasi. Kode ini akan mencegat setiap request ke endpoint yang dilindungi untuk memastikan keberadaan dan validitas token.

Buat file baru: middlewares/authenticateTokenMiddleware.js.

middlewares/authenticateTokenMiddleware.js
import jwt from "jsonwebtoken";

export const authenticateTokenMiddleware = (req, res, next) => {
    const authHeader = req.headers.authorization;

    if (!authHeader) {
        return res.status(401).send({
            error: "No token provided",
        });
    }

    // Format token biasanya "Bearer [TOKEN]", ambil elemen kedua
    const token = authHeader.split(" ")[1];
    if (!token) {
        return res.status(401).send({
            error: "No token provided"
        });
    }

    jwt.verify(token, "APP_JWT_SECRET", (err, decoded) => {
        if (err) {
            return res.status(401).send({
                error: "Invalid token",
                details: err.message,
            });
        }

        // Menyimpan data user yang terdekripsi ke dalam object request
        req.user = decoded;
        next();
    });
};

Langkah 6: Menambahkan Route

Daftarkan route untuk otentikasi dan terapkan middleware pada route movies agar hanya bisa diakses oleh pengguna yang sudah login.

Buka file api.js dan sesuaikan kodenya:

api.js
import express from "express";
import * as movieController from "../controllers/movieController.js";
import * as userController from "../controllers/userController.js";
import { authenticateTokenMiddleware } from "../middlewares/authenticateTokenMiddleware.js";

const api = express.Router();

// Public Routes (Auth)
api.post("/signin", userController.signIn);
api.post("/signup", userController.signUp);

// Protected Routes (Movies)
api.get("/movies", movieController.movies); 
api.get("/movies/:id", movieController.detailMovie);
api.post("/movies", movieController.addNewMovie);
api.put("/movies/:id", movieController.updateMovie);
api.delete("/movies/:id", movieController.deleteMovie);
api.get("/movies", authenticateTokenMiddleware, movieController.movies); 
api.get("/movies/:id", authenticateTokenMiddleware, movieController.detailMovie);
api.post("/movies", authenticateTokenMiddleware, movieController.addNewMovie);
api.put("/movies/:id", authenticateTokenMiddleware, movieController.updateMovie);
api.delete("/movies/:id", authenticateTokenMiddleware, movieController.deleteMovie);

export default api;

Langkah 7: Update Movie Model (Relasi)

Untuk menerapkan kepemilikan data, model Movie harus menyimpan referensi ID dari User yang membuatnya.

Update file models/movieModel.js:

models/movieModel.js
import mongoose from "mongoose";
import UserModel from "./userModel.js";

const MovieSchema = new mongoose.Schema(
    {
        judul: {
            type: String,
            unique: true,
            required: true,
            trim: true,
        },
        tahunRilis: {
            type: String,
            required: true,
            trim: true,
        },
        sutradara: {
            type: String,
            required: true,
            trim: true,
        },
        // Field Relasi
        createdBy: {
            type: mongoose.Types.ObjectId,
            ref: UserModel // Referensi ke model User
        }
    },
    {
        timestamps: true,
    },
);

const MovieModel = mongoose.model("movies", MovieSchema);

export default MovieModel;

Langkah 8: Update Movie Controller

Logika bisnis harus disesuaikan agar operasi CRUD (Create, Read, Update, Delete) memperhitungkan user_id dari token (req.user.user_id).

Update file controllers/movieController.js:

controllers/movieController.js
import mongoose from "mongoose";
import MovieModel from "../models/movieModel.js";

export const movies = async (req, res) => {
    try {
        // Hanya menampilkan movie milik user yang sedang login
        const movies = await MovieModel.find({}).sort({createdAt : -1}); 
        const movies = await MovieModel.find({ 
            createdBy: req.user?.user_id
        }).sort({ createdAt: -1 });

        return res.status(200).json({
            message: "Daftar semua movie",
            data: movies,
        });
    } catch (error) {
        return res.status(500).json({
            message: "Terjadi kesalahan pada server",
            error: error.message,
            data: null,
        });
    }
};

export const addNewMovie = async (req, res) => {
    try {
        const { judul, tahunRilis, sutradara } = req.body;

        if (!judul || !tahunRilis || !sutradara) {
            return res.status(400).json({
                message: "Semua field (judul, tahunRilis, sutradara) wajib diisi",
                data: null
            });
        }

        // Menyimpan user_id pembuat ke database
        const movie = await MovieModel.create({ judul, tahunRilis, sutradara }); 
        const movie = await MovieModel.create({judul, tahunRilis, sutradara, createdBy: req.user?.user_id}); 

        return res.status(201).json({
            message: "Berhasil menambahkan movie baru",
            data: movie,
        });
    } catch (error) {
        return res.status(500).json({
            message: "Gagal menambahkan movie",
            error: error.message,
            data: null,
        });
    }
};

export const detailMovie = async (req, res) => {
    try {
        const { id } = req.params;

        if (!id || !mongoose.Types.ObjectId.isValid(id)) {
            return res.status(400).json({ message: "ID tidak valid", data: null });
        }

        // Mencari movie berdasarkan ID DAN kepemilikan user
        const movie = await MovieModel.findById(id); 
        const movie = await MovieModel.findOne({  
            _id: id,
            createdBy: req.user?.user_id,
        });

        if (!movie) {
            return res.status(404).json({ message: "Movie tidak ditemukan", data: null });
        }

        return res.status(200).json({ message: "Detail movie", data: movie });
    } catch (error) {
        return res.status(500).json({
            message: "Terjadi kesalahan pada server",
            error: error.message,
            data: null,
        });
    }
};

export const updateMovie = async (req, res) => {
    try {
        const { id } = req.params;
        const { judul, tahunRilis, sutradara } = req.body;

        if (!id || !mongoose.Types.ObjectId.isValid(id)) {
            return res.status(400).json({ message: "ID tidak valid", data: null });
        }

        // Update hanya jika ID cocok DAN user pembuat cocok
        const updatedMovie = await MovieModel.findByIdAndUpdate( 
			id,
			{ judul, tahunRilis, sutradara },
			{ new: true },
		);
        const updatedMovie = await MovieModel.findOneAndUpdate( 
            {
                _id: id,
                createdBy: req.user?.user_id,
            },
            {judul, tahunRilis, sutradara},
            {new: true},
        );


        if (!updatedMovie) {
            return res.status(404).json({ message: "Movie tidak ditemukan atau akses ditolak", data: null });
        }

        return res.status(200).json({
            message: "Berhasil mengupdate movie",
            data: updatedMovie,
        });
    } catch (error) {
        return res.status(500).json({
            message: "Terjadi kesalahan pada server",
            error: error.message,
            data: null,
        });
    }
};

export const deleteMovie = async (req, res) => {
    try {
        const { id } = req.params;

        if (!id || !mongoose.Types.ObjectId.isValid(id)) {
            return res.status(400).json({ message: "ID tidak valid", data: null });
        }

        // Hapus hanya jika ID cocok DAN user pembuat cocok
        const deletedMovie = await MovieModel.findByIdAndDelete(id); 
        const deletedMovie = await MovieModel.findOneAndDelete({ 
            _id: id,
            createdBy: req.user?.user_id,
        });

        if (!deletedMovie) {
            return res.status(404).json({ message: "Movie tidak ditemukan atau akses ditolak", data: null });
        }

        return res.status(200).json({
            message: "Berhasil menghapus movie",
            data: deletedMovie,
        });
    } catch (error) {
        return res.status(500).json({
            message: "Terjadi kesalahan pada server",
            error: error.message,
            data: null,
        });
    }
};

Langkah Test

Silakan lakukan pengujian menggunakan Postman:

  1. Lakukan POST /signup untuk membuat user.
  2. Lakukan POST /signin untuk mendapatkan token.
  3. Gunakan token tersebut pada tab Authorization (Bearer Token) di Postman untuk mengakses endpoint /movies.