Hoàn thiện ứng dụng quản lý phim, xác thực và triển khai Render
- 24-07-2026
- Toanngo92
- 0 Comments
Bài 11 đã dựng bộ khung Wonderland Widescreen và các route đầu tiên. Trong bài này, chúng ta hoàn thiện ứng dụng quản lý phim: thêm, cập nhật, xóa phim, đặt ghế, đăng nhập theo vai trò, duy trì phiên làm việc và triển khai ứng dụng Node.js lên Render từ GitHub.
Giáo trình sử dụng tài khoản minh họa được viết trực tiếp trong mã nguồn. Bài viết sẽ giữ nguyên luồng chức năng nhưng thay bằng cách triển khai an toàn hơn: mật khẩu được băm, bí mật nằm trong biến môi trường và session được lưu trong MongoDB.
Mục lục
1. Các chức năng cần hoàn thiện
- Khách đăng nhập, xem danh sách phim và đặt ghế.
- Quản trị viên thêm, cập nhật hoặc xóa phim.
- Ứng dụng ghi nhớ người dùng giữa các request bằng session.
- Route được bảo vệ theo trạng thái đăng nhập và vai trò.
- Mã nguồn được đẩy lên GitHub và triển khai thành Web Service trên Render.

2. Cài đặt các gói xác thực và session
npm install passport passport-local express-session connect-mongo bcrypt dotenv
Passport chỉ phụ trách xác thực request. express-session tạo session và đặt một mã định danh trong cookie; dữ liệu phiên nằm phía máy chủ. Không dùng MemoryStore mặc định khi triển khai thật vì nó không phù hợp cho nhiều tiến trình và sẽ mất dữ liệu khi dịch vụ khởi động lại.
Tạo thêm các biến môi trường sau. Không commit tệp .env lên GitHub.
MONGODB_URI=mongodb+srv://<username>:<password>@<cluster>/
DB_NAME=MovieCollection
SESSION_SECRET=thay-bang-chuoi-ngau-nhien-dai
PORT=3000
NODE_ENV=development
3. Cấu hình session cho Express
const session = require('express-session');
const MongoStore = require('connect-mongo');
const passport = require('passport');
if (process.env.NODE_ENV === 'production') {
app.set('trust proxy', 1);
}
app.use(session({
name: 'wonderland.sid',
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI,
dbName: process.env.DB_NAME
}),
cookie: {
httpOnly: true,
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
maxAge: 1000 * 60 * 60 * 8
}
}));
app.use(passport.initialize());
app.use(passport.session());
Thứ tự middleware rất quan trọng: session phải được khởi tạo trước passport.session(). Cookie chỉ chứa mã phiên, đặt httpOnly để JavaScript phía trình duyệt không đọc được và bật secure khi chạy qua HTTPS.
4. Tạo người dùng và cấu hình Passport Local
Mỗi tài khoản nên có tối thiểu username, passwordHash và role. Khi tạo tài khoản, băm mật khẩu bằng bcrypt thay vì lưu chuỗi gốc.
const bcrypt = require('bcrypt');
const admin = {
username: 'admin',
passwordHash: await bcrypt.hash(process.env.ADMIN_PASSWORD, 12),
role: 'admin'
};
await users.updateOne(
{ username: admin.username },
{ $setOnInsert: admin },
{ upsert: true }
);
const { Strategy: LocalStrategy } = require('passport-local');
const { ObjectId } = require('mongodb');
passport.use(new LocalStrategy(async (username, password, done) => {
try {
const user = await users.findOne({ username });
if (!user) return done(null, false, { message: 'Sai thông tin đăng nhập' });
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) return done(null, false, { message: 'Sai thông tin đăng nhập' });
return done(null, user);
} catch (error) {
return done(error);
}
}));
passport.serializeUser((user, done) => done(null, user._id.toString()));
passport.deserializeUser(async (id, done) => {
try {
done(null, await users.findOne({ _id: new ObjectId(id) }));
} catch (error) {
done(error);
}
});

5. Route đăng nhập, đăng xuất và phân quyền
app.get('/login', (req, res) => res.render('login'));
app.post('/login', passport.authenticate('local', {
failureRedirect: '/login'
}), (req, res) => {
res.redirect(req.user.role === 'admin' ? '/admin' : '/movies');
});
app.post('/logout', (req, res, next) => {
req.logout((error) => {
if (error) return next(error);
req.session.destroy(() => {
res.clearCookie('wonderland.sid');
res.redirect('/');
});
});
});
function requireLogin(req, res, next) {
if (req.isAuthenticated()) return next();
res.redirect('/login');
}
function requireRole(role) {
return (req, res, next) => {
if (!req.isAuthenticated()) return res.redirect('/login');
if (req.user.role !== role) return res.sendStatus(403);
next();
};
}
app.get('/movies', requireLogin, showMovies);
app.get('/admin', requireRole('admin'), showAdminDashboard);
Ẩn nút trên giao diện không phải là phân quyền. Tất cả route thay đổi dữ liệu vẫn phải kiểm tra vai trò ở máy chủ. Nên dùng biểu mẫu POST cho đăng xuất thay vì một liên kết GET.

6. Hoàn thiện thêm, cập nhật và xóa phim
app.post('/admin/movies', requireRole('admin'), async (req, res, next) => {
try {
const movie = normalizeMovie(req.body);
await movies.insertOne(movie);
res.redirect('/admin');
} catch (error) {
next(error);
}
});
app.post('/admin/movies/:id/seats', requireRole('admin'), async (req, res, next) => {
try {
const availableSeats = Number(req.body.availableSeats);
if (!Number.isInteger(availableSeats) || availableSeats < 0) {
return res.status(400).send('Số ghế không hợp lệ');
}
await movies.updateOne(
{ _id: new ObjectId(req.params.id) },
{ $set: { availableSeats } }
);
res.redirect('/admin');
} catch (error) {
next(error);
}
});
app.post('/admin/movies/:id/delete', requireRole('admin'), async (req, res, next) => {
try {
await movies.deleteOne({ _id: new ObjectId(req.params.id) });
res.redirect('/admin');
} catch (error) {
next(error);
}
});


Ví dụ dùng POST để tương thích với form HTML. Trong API chuẩn có thể dùng PATCH và DELETE. Hãy thêm CSRF token cho mọi biểu mẫu thay đổi dữ liệu trước khi đưa ứng dụng vào sử dụng thực tế.
7. Đặt ghế an toàn bằng thao tác nguyên tử

app.post('/movies/:id/book', requireLogin, async (req, res, next) => {
try {
const seats = Number(req.body.seats);
if (!Number.isInteger(seats) || seats < 1) {
return res.status(400).send('Số ghế đặt không hợp lệ');
}
const result = await movies.updateOne(
{
_id: new ObjectId(req.params.id),
availableSeats: { $gte: seats }
},
{ $inc: { availableSeats: -seats } }
);
if (result.modifiedCount === 0) {
return res.status(409).send('Không còn đủ ghế');
}
res.redirect('/movies');
} catch (error) {
next(error);
}
});
Không nên đọc số ghế rồi cập nhật bằng hai câu lệnh tách rời vì hai người có thể đặt cùng lúc. Điều kiện availableSeats: { $gte: seats } và toán tử $inc thực hiện kiểm tra cùng cập nhật trong một thao tác nguyên tử.
8. Chuẩn bị ứng dụng để triển khai
Thêm script khởi động và yêu cầu phiên bản Node.js vào package.json:
{
"scripts": {
"start": "node server.js"
},
"engines": {
"node": ">=20"
}
}
const port = process.env.PORT || 3000;
app.listen(port, '0.0.0.0', () => {
console.log(`Server đang chạy trên cổng ${port}`);
});
Tệp .gitignore tối thiểu nên loại bỏ node_modules/, .env, log và tệp hệ điều hành. Chạy ứng dụng cục bộ với NODE_ENV=production để phát hiện sớm lỗi cookie hoặc biến môi trường.
9. Đẩy mã nguồn lên GitHub
git init
git add .
git commit -m "Hoan thien Wonderland Widescreen"
git branch -M main
git remote add origin https://github.com/USERNAME/movie-application.git
git push -u origin main
Trước khi push, dùng git status và kiểm tra chắc chắn .env không nằm trong danh sách commit. Nếu bí mật từng được commit, xóa tệp ở commit mới là chưa đủ; cần thu hồi và tạo lại thông tin đăng nhập ngay.
10. Triển khai Web Service trên Render
- Trong Render Dashboard, chọn New → Web Service.
- Kết nối repository GitHub chứa ứng dụng.
- Chọn runtime Node, đặt Build Command là
npm civà Start Command lànpm start. - Thêm
MONGODB_URI,DB_NAME,SESSION_SECRET,ADMIN_PASSWORDvàNODE_ENV=productiontrong phần Environment. - Triển khai, mở URL
onrender.comvà kiểm tra đăng nhập, CRUD, đặt ghế, đăng xuất.
Render có thể tự động build và triển khai lại khi branch đã kết nối nhận commit mới. Không đặt biến PORT cố định trên dịch vụ; mã nguồn phải đọc cổng do nền tảng cung cấp.
11. Danh sách kiểm tra trước khi bàn giao
- Khách không truy cập được route quản trị.
- Quản trị viên thêm, cập nhật và xóa phim thành công.
- Không thể đặt số ghế âm, bằng 0 hoặc lớn hơn số ghế còn lại.
- Đăng xuất hủy session và cookie.
- Mật khẩu, MongoDB URI và session secret không xuất hiện trong repository hoặc log.
- Trang 404 và middleware xử lý lỗi hoạt động.
- Ứng dụng chạy đúng sau một lần redeploy hoặc restart.
Tổng kết
Wonderland Widescreen giờ đã có đầy đủ luồng người dùng và quản trị: đăng nhập theo session, phân quyền route, CRUD phim, đặt ghế an toàn và triển khai lên môi trường cloud. Đây cũng là cấu trúc nền tảng có thể áp dụng cho ứng dụng quản lý sinh viên, đặt lịch hoặc quản lý sản phẩm.
