Commit aba86181 by tdgiang

Fix critical/important findings from final whole-branch review

parent 5342bbfe
......@@ -206,7 +206,7 @@ exports.listTransactions = function (req, res) {
if (!isAdmin) {
filter.createdByUsername = req.session.username;
} else if (req.query.staff) {
filter.createdByUsername = req.query.staff;
filter.createdByUsername = String(req.query.staff);
}
function render(staffOptions) {
......
......@@ -2,6 +2,14 @@
var AdminUser = require("../models/AdminUser");
var passwordHash = require("../libs/passwordHash");
var MIN_PASSWORD_LENGTH = 8;
// Fixed, precomputed bcrypt hash of a value nobody will ever type as a real password.
// Used only to make the "unknown username" path cost the same time as "wrong password" —
// comparing against this constant takes the same ~70ms as a real compare, so an attacker
// timing responses cannot tell "no such user" apart from "wrong password".
var DUMMY_HASH = "$2b$10$v5huvbowopyU7O11oK7ykeCMpvpEJCt4.y9cv8kZ.7liCwdYEY8sS";
exports.loginForm = function (req, res) {
res.render("admin/login", { error: null });
};
......@@ -15,17 +23,38 @@ exports.login = function (req, res) {
console.error("login: DB error:", err.message);
return res.status(503).render("admin/login", { error: "Hệ thống đang bận, vui lòng thử lại sau" });
}
if (!user || !passwordHash.compare(password, user.passwordHash)) {
var hashToCompare = user ? user.passwordHash : DUMMY_HASH;
passwordHash.compare(password, hashToCompare, function (compareErr, isMatch) {
if (compareErr) {
console.error("login: compare error:", compareErr.message);
return res.status(503).render("admin/login", { error: "Hệ thống đang bận, vui lòng thử lại sau" });
}
if (!user || !isMatch) {
return res.render("admin/login", { error: "Sai tên đăng nhập hoặc mật khẩu" });
}
if (!user.active) {
return res.render("admin/login", { error: "Tài khoản đã bị khoá, liên hệ admin" });
}
req.session.regenerate(function (regenErr) {
if (regenErr) {
console.error("login: session regenerate error:", regenErr.message);
return res.status(503).render("admin/login", { error: "Hệ thống đang bận, vui lòng thử lại sau" });
}
req.session.userId = user._id.toString();
req.session.username = user.username;
req.session.role = user.role;
req.session.save(function (saveErr) {
if (saveErr) {
console.error("login: session save error:", saveErr.message);
return res.status(503).render("admin/login", { error: "Hệ thống đang bận, vui lòng thử lại sau" });
}
return res.redirect("/admin/transactions");
});
});
});
});
};
exports.logout = function (req, res) {
......@@ -58,11 +87,19 @@ exports.createAccount = function (req, res) {
if (!username || !password) {
return res.redirect("/admin/accounts?error=MISSING_FIELDS");
}
if (password.length < MIN_PASSWORD_LENGTH) {
return res.redirect("/admin/accounts?error=PASSWORD_TOO_SHORT");
}
passwordHash.hash(password, function (hashErr, hashed) {
if (hashErr) {
console.error("createAccount: hash error:", hashErr.message);
return res.redirect("/admin/accounts?error=DB_ERROR");
}
AdminUser.create(
{
username: username,
passwordHash: passwordHash.hash(password),
passwordHash: hashed,
role: role,
active: true,
},
......@@ -77,9 +114,14 @@ exports.createAccount = function (req, res) {
return res.redirect("/admin/accounts?created=1");
}
);
});
};
exports.toggleAccount = function (req, res) {
if (req.params.id === req.session.userId) {
return res.redirect("/admin/accounts?error=CANNOT_LOCK_SELF");
}
AdminUser.findById(req.params.id, function (err, user) {
if (err) {
console.error("toggleAccount: DB error:", err.message);
......@@ -88,7 +130,11 @@ exports.toggleAccount = function (req, res) {
if (!user) {
return res.status(404).send("Không tìm thấy tài khoản");
}
user.active = !user.active;
var nextActive = !user.active;
function applyToggle() {
user.active = nextActive;
user.save(function (saveErr) {
if (saveErr) {
console.error("toggleAccount: DB error saving:", saveErr.message);
......@@ -96,6 +142,25 @@ exports.toggleAccount = function (req, res) {
}
return res.redirect("/admin/accounts");
});
}
if (!nextActive && user.role === "admin") {
AdminUser.countDocuments(
{ role: "admin", active: true, _id: { $ne: user._id } },
function (countErr, remaining) {
if (countErr) {
console.error("toggleAccount: DB error counting admins:", countErr.message);
return res.redirect("/admin/accounts?error=DB_ERROR");
}
if (remaining < 1) {
return res.redirect("/admin/accounts?error=LAST_ADMIN");
}
applyToggle();
}
);
} else {
applyToggle();
}
});
};
......@@ -104,6 +169,9 @@ exports.resetAccountPassword = function (req, res) {
if (!newPassword) {
return res.redirect("/admin/accounts?error=MISSING_FIELDS");
}
if (newPassword.length < MIN_PASSWORD_LENGTH) {
return res.redirect("/admin/accounts?error=PASSWORD_TOO_SHORT");
}
AdminUser.findById(req.params.id, function (err, user) {
if (err) {
console.error("resetAccountPassword: DB error:", err.message);
......@@ -112,7 +180,12 @@ exports.resetAccountPassword = function (req, res) {
if (!user) {
return res.status(404).send("Không tìm thấy tài khoản");
}
user.passwordHash = passwordHash.hash(newPassword);
passwordHash.hash(newPassword, function (hashErr, hashed) {
if (hashErr) {
console.error("resetAccountPassword: hash error:", hashErr.message);
return res.redirect("/admin/accounts?error=DB_ERROR");
}
user.passwordHash = hashed;
user.save(function (saveErr) {
if (saveErr) {
console.error("resetAccountPassword: DB error saving:", saveErr.message);
......@@ -121,6 +194,7 @@ exports.resetAccountPassword = function (req, res) {
return res.redirect("/admin/accounts?reset=1");
});
});
});
};
exports.changePasswordForm = function (req, res) {
......@@ -136,13 +210,29 @@ exports.changePassword = function (req, res) {
console.error("changePassword: DB error:", err.message);
return res.status(503).render("admin/change-password", { error: "Hệ thống đang bận, vui lòng thử lại sau", success: false });
}
if (!user || !passwordHash.compare(currentPassword, user.passwordHash)) {
if (!user) {
return res.status(503).render("admin/change-password", { error: "Hệ thống đang bận, vui lòng thử lại sau", success: false });
}
passwordHash.compare(currentPassword, user.passwordHash, function (compareErr, isMatch) {
if (compareErr) {
console.error("changePassword: compare error:", compareErr.message);
return res.status(503).render("admin/change-password", { error: "Hệ thống đang bận, vui lòng thử lại sau", success: false });
}
if (!isMatch) {
return res.render("admin/change-password", { error: "Mật khẩu hiện tại không đúng", success: false });
}
if (!newPassword) {
return res.render("admin/change-password", { error: "Vui lòng nhập mật khẩu mới", success: false });
}
user.passwordHash = passwordHash.hash(newPassword);
if (newPassword.length < MIN_PASSWORD_LENGTH) {
return res.render("admin/change-password", { error: "Mật khẩu mới phải có ít nhất 8 ký tự", success: false });
}
passwordHash.hash(newPassword, function (hashErr, hashed) {
if (hashErr) {
console.error("changePassword: hash error:", hashErr.message);
return res.status(500).render("admin/change-password", { error: "Lỗi cơ sở dữ liệu", success: false });
}
user.passwordHash = hashed;
user.save(function (saveErr) {
if (saveErr) {
console.error("changePassword: DB error saving:", saveErr.message);
......@@ -151,4 +241,6 @@ exports.changePassword = function (req, res) {
return res.render("admin/change-password", { error: null, success: true });
});
});
});
});
};
......@@ -15,10 +15,14 @@ module.exports = function run(config, callback) {
console.error("adminBootstrap: no admin exists and ADMIN_USER/ADMIN_PASSWORD not set - cannot seed");
return callback(null);
}
passwordHash.hash(config.admin.password, function (hashErr, hashed) {
if (hashErr) {
return callback(hashErr);
}
AdminUser.create(
{
username: String(config.admin.user).trim().toLowerCase(),
passwordHash: passwordHash.hash(config.admin.password),
passwordHash: hashed,
role: "admin",
active: true,
},
......@@ -31,6 +35,7 @@ module.exports = function run(config, callback) {
}
);
});
});
};
function backfillTransactions(adminUsername, callback) {
......
"use strict";
var bcrypt = require("bcryptjs");
function hash(plain) {
return bcrypt.hashSync(plain, 10);
function hash(plain, callback) {
bcrypt.hash(plain, 10, callback);
}
function compare(plain, hashed) {
return bcrypt.compareSync(plain, hashed);
function compare(plain, hashed, callback) {
bcrypt.compare(plain, hashed, callback);
}
module.exports = {
......
"use strict";
var AdminUser = require("../models/AdminUser");
module.exports = function requireLogin(req, res, next) {
if (req.session && req.session.userId) {
return next();
if (!req.session || !req.session.userId) {
if (req.is("json")) {
return res.status(401).json({ code: "99", data: "LOGIN_REQUIRED" });
}
return res.redirect("/admin/login");
}
AdminUser.findById(req.session.userId, function (err, user) {
if (err) {
console.error("requireLogin: DB error:", err.message);
if (req.is("json")) {
return res.status(503).json({ code: "99", data: "SERVICE_UNAVAILABLE" });
}
return res.status(503).send("Hệ thống đang bận, vui lòng thử lại sau.");
}
if (!user || !user.active) {
return req.session.destroy(function () {
if (req.is("json")) {
return res.status(401).json({ code: "99", data: "LOGIN_REQUIRED" });
}
return res.redirect("/admin/login");
});
}
// Keep the session's cached role/username in sync with the DB on every request,
// in case an admin changed either after this session was created.
req.session.role = user.role;
req.session.username = user.username;
return next();
});
};
......@@ -100,6 +100,9 @@
{% if error == "MISSING_FIELDS" %}<div class="alert alert-error">Vui lòng nhập đủ thông tin.</div>{% endif %}
{% if error == "DUPLICATE_USERNAME" %}<div class="alert alert-error">Tên đăng nhập đã tồn tại.</div>{% endif %}
{% if error == "DB_ERROR" %}<div class="alert alert-error">Lỗi cơ sở dữ liệu, vui lòng thử lại.</div>{% endif %}
{% if error == "PASSWORD_TOO_SHORT" %}<div class="alert alert-error">Mật khẩu phải có ít nhất 8 ký tự.</div>{% endif %}
{% if error == "CANNOT_LOCK_SELF" %}<div class="alert alert-error">Bạn không thể khoá tài khoản của chính mình.</div>{% endif %}
{% if error == "LAST_ADMIN" %}<div class="alert alert-error">Không thể khoá — đây là tài khoản admin đang hoạt động cuối cùng.</div>{% endif %}
<div class="card">
<h2>Tạo tài khoản nhân viên mới</h2>
......@@ -111,7 +114,7 @@
</div>
<div class="form-field">
<label for="password">Mật khẩu</label>
<input type="password" id="password" name="password" required>
<input type="password" id="password" name="password" required minlength="8">
</div>
<div class="form-field" style="max-width:140px;">
<label for="role">Vai trò</label>
......@@ -144,11 +147,15 @@
<td><span class="pill pill-{% if u.active %}active{% else %}locked{% endif %}">{% if u.active %}Hoạt động{% else %}Đã khoá{% endif %}</span></td>
<td>
<div class="row-actions">
{% if u.username == currentUsername %}
<span style="font-size:12.5px;color:var(--color-text-muted);">Tài khoản của bạn</span>
{% else %}
<form method="POST" action="/admin/accounts/{{u._id}}/toggle">
<button type="submit" class="secondary">{% if u.active %}Khoá{% else %}Mở khoá{% endif %}</button>
</form>
{% endif %}
<form method="POST" action="/admin/accounts/{{u._id}}/reset-password">
<input type="password" name="newPassword" placeholder="Mật khẩu mới" required>
<input type="password" name="newPassword" placeholder="Mật khẩu mới" required minlength="8">
<button type="submit" class="secondary">Đặt lại</button>
</form>
</div>
......
......@@ -71,9 +71,9 @@
</tbody>
</table>
<p class="pager">
{% if hasPrev %}<a href="/admin/transactions?page={{prevPage}}">&laquo; Trang trước</a>{% endif %}
{% if hasPrev %}<a href="/admin/transactions?page={{prevPage}}{% if selectedStaff %}&staff={{selectedStaff}}{% endif %}">&laquo; Trang trước</a>{% endif %}
Trang {{page}}
{% if hasNext %}<a href="/admin/transactions?page={{nextPage}}">Trang sau &raquo;</a>{% endif %}
{% if hasNext %}<a href="/admin/transactions?page={{nextPage}}{% if selectedStaff %}&staff={{selectedStaff}}{% endif %}">Trang sau &raquo;</a>{% endif %}
</p>
</body>
</html>
......@@ -100,6 +100,7 @@ module.exports = function() {
cookie: {
secure: cookieSecure,
httpOnly: true,
sameSite: 'lax',
maxAge: config.session_max_age
},
// store: new mongoStore({
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment