Implementasi Autentikasi dan Relasi Database pada MongoDB dengan Mongoose
Gunakan project movie yang telah kalian buat pada pertemuan sebelumnya
Pendahuluan
Tutorial ini berfokus pada pengembangan fitur keamanan dan struktur data pada aplikasi Movie Express. Dua konsep dasar yang akan diimplementasikan adalah:
- Authentication (Otentikasi): Mekanisme verifikasi identitas pengguna menggunakan JSON Web Token (JWT).
- Database Relations (Relasi Database): Menghubungkan data antar koleksi (collections) MongoDB, spesifiknya antara
MovieModeldanUserModel.
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 bcryptPenjelasan
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.
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 fieldcreatedAtdanupdatedAt.
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
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
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.
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.
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:
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:
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:
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:
- Lakukan
POST /signupuntuk membuat user. - Lakukan
POST /signinuntuk mendapatkantoken. - Gunakan token tersebut pada tab Authorization (Bearer Token) di Postman untuk mengakses endpoint
/movies.