Commit 557e8965 by tdgiang

Fix critical/important findings from final whole-branch review

parent 8f6e76b9
......@@ -13,12 +13,17 @@ exports.createTransaction = function (req, res) {
var customerName = req.body.customerName;
var customerPhone = req.body.customerPhone;
var customerAddress = req.body.customerAddress;
var amount = req.body.amount;
var rawAmount = req.body.amount;
if (!customerName || !customerPhone || !customerAddress || !amount) {
if (!customerName || !customerPhone || !customerAddress || !rawAmount) {
return res.status(400).json({ code: "99", data: "MISSING_FIELDS" });
}
var amount = parseInt(rawAmount, 10);
if (!isFinite(amount) || amount <= 0 || String(amount) !== String(rawAmount).trim()) {
return res.status(400).json({ code: "99", data: "INVALID_AMOUNT" });
}
var timeStamp = moment().format("YYYYMMDDHHmmss");
var uniqueSuffix = uuidv4().split("-")[0];
var merTrxId = "HY_" + timeStamp + "_" + uniqueSuffix;
......@@ -38,12 +43,13 @@ exports.createTransaction = function (req, res) {
customerName: customerName,
customerPhone: customerPhone,
customerAddress: customerAddress,
amount: parseInt(amount),
amount: amount,
merchantToken: merchantToken,
timeStamp: timeStamp,
},
function (err, tx) {
if (err) {
console.error("createTransaction: DB error:", err.message);
return res.status(500).json({ code: "99", data: "DB_ERROR" });
}
var paymentUrl = config.epay.req_domain + "/admin/pay/" + tx.merTrxId;
......@@ -57,11 +63,16 @@ exports.createTransaction = function (req, res) {
exports.payPage = function (req, res) {
AdminTransaction.findOne({ merTrxId: req.params.merTrxId }, function (err, tx) {
if (err || !tx) {
if (err) {
console.error("payPage: DB error for merTrxId=" + req.params.merTrxId + ":", err.message);
return res.status(503).render("admin/pay", { notFound: true });
}
if (!tx) {
return res.status(404).render("admin/pay", { notFound: true });
}
res.render("admin/pay", {
notFound: false,
alreadyPaid: tx.status !== "pending",
tx: tx,
domain: config.epay.domain,
merId: config.epay.merchant_id,
......@@ -85,7 +96,14 @@ exports.epayReturn = function (req, res) {
}
AdminTransaction.findOne({ merTrxId: merTrxId }, function (err, tx) {
if (err || !tx) {
if (err) {
console.error("epayReturn: DB error for merTrxId=" + merTrxId + ":", err.message);
return res.status(503).render("admin/pay-result", {
success: false,
message: "Hệ thống đang bận, vui lòng thử lại sau",
});
}
if (!tx) {
return res.render("admin/pay-result", {
success: false,
message: "Không tìm thấy giao dịch",
......@@ -104,6 +122,7 @@ exports.epayReturn = function (req, res) {
);
if (expected !== q.merchantToken) {
console.error("epayReturn: signature mismatch for merTrxId=" + merTrxId);
return res.render("admin/pay-result", {
success: false,
message: "Chữ ký không đúng",
......@@ -123,7 +142,12 @@ exports.epayIPN = function (req, res) {
var merTrxId = b.merTrxId;
AdminTransaction.findOne({ merTrxId: merTrxId }, function (err, tx) {
if (err || !tx) {
if (err) {
console.error("epayIPN: DB error for merTrxId=" + merTrxId + ":", err.message);
return res.status(503).json({ code: "99", data: "DB_UNAVAILABLE" });
}
if (!tx) {
console.error("epayIPN: TRX_NOT_FOUND for merTrxId=" + merTrxId);
return res.status(400).json({ code: "99", data: "TRX_NOT_FOUND" });
}
......@@ -139,12 +163,19 @@ exports.epayIPN = function (req, res) {
);
if (expected !== b.merchantToken) {
console.error("epayIPN: INVALID_SIGNATURE for merTrxId=" + merTrxId + " resultCd=" + resultCd);
return res.status(400).json({ code: "99", data: "INVALID_SIGNATURE" });
}
if (tx.status !== "pending") {
// Already terminal - acknowledge without re-processing. Never let a later IPN
// (retry, or a duplicate payment attempt) downgrade an already-settled result.
return res.status(200).json({ code: "00", data: "Success" });
}
var newStatus = resultCd === "00_000" ? "success" : "failed";
AdminTransaction.updateOne(
{ merTrxId: merTrxId },
{ merTrxId: merTrxId, status: "pending" },
{
status: newStatus,
resultMsg: b.resultMsg || "",
......@@ -152,6 +183,7 @@ exports.epayIPN = function (req, res) {
},
function (updateErr) {
if (updateErr) {
console.error("epayIPN: DB error updating merTrxId=" + merTrxId + ":", updateErr.message);
return res.status(500).json({ code: "99", data: "DB_ERROR" });
}
return res.status(200).json({ code: "00", data: "Success" });
......@@ -170,6 +202,7 @@ exports.listTransactions = function (req, res) {
.limit(perPage)
.exec(function (err, list) {
if (err) {
console.error("listTransactions: DB error:", err.message);
return res.status(500).send("Lỗi cơ sở dữ liệu");
}
var transactions = list.map(function (tx) {
......@@ -187,6 +220,10 @@ exports.listTransactions = function (req, res) {
res.render("admin/transactions-list", {
transactions: transactions,
page: page,
prevPage: page - 1,
nextPage: page + 1,
hasPrev: page > 1,
hasNext: transactions.length === perPage,
});
});
};
"use strict";
var crypto = require("crypto");
function safeEqual(a, b) {
var bufA = Buffer.from(String(a));
var bufB = Buffer.from(String(b));
if (bufA.length !== bufB.length) {
crypto.timingSafeEqual(bufA, bufA);
return false;
}
return crypto.timingSafeEqual(bufA, bufB);
}
module.exports = function basicAuth(req, res, next) {
var config = require(__config_path + "/config");
var expectedUser = config.admin && config.admin.user;
var expectedPassword = config.admin && config.admin.password;
if (!expectedUser || !expectedPassword) {
console.error("basicAuth: ADMIN_USER/ADMIN_PASSWORD not configured - denying all admin access");
return res.status(500).send("Admin auth not configured.");
}
var header = req.headers.authorization || "";
var token = header.indexOf("Basic ") === 0 ? header.slice(6) : "";
var token = header.slice(0, 6).toLowerCase() === "basic " ? header.slice(6) : "";
var decoded = Buffer.from(token, "base64").toString("utf8");
var parts = decoded.split(":");
var user = parts[0];
var password = parts[1];
var sepIndex = decoded.indexOf(":");
var user = sepIndex === -1 ? decoded : decoded.slice(0, sepIndex);
var password = sepIndex === -1 ? "" : decoded.slice(sepIndex + 1);
if (user === config.admin.user && password === config.admin.password) {
if (safeEqual(user, expectedUser) && safeEqual(password, expectedPassword)) {
return next();
}
......
......@@ -4,7 +4,7 @@ var Schema = mongoose.Schema;
var AdminTransactionSchema = new Schema(
{
merTrxId: { type: String, required: true, unique: true, index: true },
merTrxId: { type: String, required: true, unique: true },
transCode: { type: String, required: true },
customerName: { type: String, required: true },
customerPhone: { type: String, required: true },
......
......@@ -12,6 +12,10 @@
<h2>Không tìm thấy giao dịch</h2>
<p>Đường dẫn thanh toán không hợp lệ hoặc đã hết hạn.</p>
{% else %}
{% if alreadyPaid %}
<h2>Giao dịch này đã được xử lý</h2>
<p>Trạng thái: {{tx.status}}</p>
{% else %}
<h2>Thanh toán {{tx.amount}} VNĐ</h2>
<p>Khách hàng: {{tx.customerName}}</p>
<form id="megapayForm" name="megapayForm" method="POST">
......@@ -36,5 +40,6 @@
<script src="/js/jquery.min.js"></script>
<script src="{{domain}}/pg_was/js/payment/layer/paymentClient.js"></script>
{% endif %}
{% endif %}
</body>
</html>
......@@ -4,13 +4,15 @@
<meta charset="utf-8">
<title>Lịch sử giao dịch</title>
<style>
body { font-family: Arial, sans-serif; max-width: 900px; margin: 40px auto; }
body { font-family: Arial, sans-serif; max-width: 1000px; margin: 40px auto; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; font-size: 14px; }
th { background: #f0f0f0; }
.status-pending { color: #b8860b; }
.status-success { color: #276b45; }
.status-failed { color: #a3352a; }
.pager { margin-top: 16px; }
.pager a { margin-right: 12px; }
</style>
</head>
<body>
......@@ -20,10 +22,12 @@
<thead>
<tr>
<th>Thời gian</th>
<th>Mã giao dịch</th>
<th>Khách hàng</th>
<th>SĐT</th>
<th>Số tiền</th>
<th>Trạng thái</th>
<th>Ghi chú</th>
<th>Link thanh toán</th>
</tr>
</thead>
......@@ -31,14 +35,21 @@
{% for tx in transactions %}
<tr>
<td>{{tx.createdAt}}</td>
<td>{{tx.merTrxId}}</td>
<td>{{tx.customerName}}</td>
<td>{{tx.customerPhone}}</td>
<td>{{tx.amount}}</td>
<td class="status-{{tx.status}}">{{tx.status}}</td>
<td>{{tx.resultMsg}}</td>
<td><a href="{{tx.paymentUrl}}" target="_blank">Link</a></td>
</tr>
{% endfor %}
</tbody>
</table>
<p class="pager">
{% if hasPrev %}<a href="/admin/transactions?page={{prevPage}}">&laquo; Trang trước</a>{% endif %}
Trang {{page}}
{% if hasNext %}<a href="/admin/transactions?page={{nextPage}}">Trang sau &raquo;</a>{% endif %}
</p>
</body>
</html>
......@@ -12,9 +12,13 @@ var init = require('./config/init')(),
// Avoids DEPTH_ZERO_SELF_SIGNED_CERT error for self-signed certs
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
mongoose.set('bufferCommands', false);
mongoose.connect(config.mongoUri, {
useNewUrlParser: true,
useUnifiedTopology: true
useUnifiedTopology: true,
serverSelectionTimeoutMS: 5000
}).catch(function (err) {
console.error('MongoDB initial connection failed (admin module degraded):', err.message);
});
mongoose.connection.on('error', function (err) {
console.error('MongoDB connection error:', err.message);
......
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