Commit 557e8965 by tdgiang

Fix critical/important findings from final whole-branch review

parent 8f6e76b9
...@@ -13,12 +13,17 @@ exports.createTransaction = function (req, res) { ...@@ -13,12 +13,17 @@ exports.createTransaction = function (req, res) {
var customerName = req.body.customerName; var customerName = req.body.customerName;
var customerPhone = req.body.customerPhone; var customerPhone = req.body.customerPhone;
var customerAddress = req.body.customerAddress; 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" }); 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 timeStamp = moment().format("YYYYMMDDHHmmss");
var uniqueSuffix = uuidv4().split("-")[0]; var uniqueSuffix = uuidv4().split("-")[0];
var merTrxId = "HY_" + timeStamp + "_" + uniqueSuffix; var merTrxId = "HY_" + timeStamp + "_" + uniqueSuffix;
...@@ -38,12 +43,13 @@ exports.createTransaction = function (req, res) { ...@@ -38,12 +43,13 @@ exports.createTransaction = function (req, res) {
customerName: customerName, customerName: customerName,
customerPhone: customerPhone, customerPhone: customerPhone,
customerAddress: customerAddress, customerAddress: customerAddress,
amount: parseInt(amount), amount: amount,
merchantToken: merchantToken, merchantToken: merchantToken,
timeStamp: timeStamp, timeStamp: timeStamp,
}, },
function (err, tx) { function (err, tx) {
if (err) { if (err) {
console.error("createTransaction: DB error:", err.message);
return res.status(500).json({ code: "99", data: "DB_ERROR" }); return res.status(500).json({ code: "99", data: "DB_ERROR" });
} }
var paymentUrl = config.epay.req_domain + "/admin/pay/" + tx.merTrxId; var paymentUrl = config.epay.req_domain + "/admin/pay/" + tx.merTrxId;
...@@ -57,11 +63,16 @@ exports.createTransaction = function (req, res) { ...@@ -57,11 +63,16 @@ exports.createTransaction = function (req, res) {
exports.payPage = function (req, res) { exports.payPage = function (req, res) {
AdminTransaction.findOne({ merTrxId: req.params.merTrxId }, function (err, tx) { 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 }); return res.status(404).render("admin/pay", { notFound: true });
} }
res.render("admin/pay", { res.render("admin/pay", {
notFound: false, notFound: false,
alreadyPaid: tx.status !== "pending",
tx: tx, tx: tx,
domain: config.epay.domain, domain: config.epay.domain,
merId: config.epay.merchant_id, merId: config.epay.merchant_id,
...@@ -85,7 +96,14 @@ exports.epayReturn = function (req, res) { ...@@ -85,7 +96,14 @@ exports.epayReturn = function (req, res) {
} }
AdminTransaction.findOne({ merTrxId: merTrxId }, function (err, tx) { 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", { return res.render("admin/pay-result", {
success: false, success: false,
message: "Không tìm thấy giao dịch", message: "Không tìm thấy giao dịch",
...@@ -104,6 +122,7 @@ exports.epayReturn = function (req, res) { ...@@ -104,6 +122,7 @@ exports.epayReturn = function (req, res) {
); );
if (expected !== q.merchantToken) { if (expected !== q.merchantToken) {
console.error("epayReturn: signature mismatch for merTrxId=" + merTrxId);
return res.render("admin/pay-result", { return res.render("admin/pay-result", {
success: false, success: false,
message: "Chữ ký không đúng", message: "Chữ ký không đúng",
...@@ -123,7 +142,12 @@ exports.epayIPN = function (req, res) { ...@@ -123,7 +142,12 @@ exports.epayIPN = function (req, res) {
var merTrxId = b.merTrxId; var merTrxId = b.merTrxId;
AdminTransaction.findOne({ merTrxId: merTrxId }, function (err, tx) { 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" }); return res.status(400).json({ code: "99", data: "TRX_NOT_FOUND" });
} }
...@@ -139,12 +163,19 @@ exports.epayIPN = function (req, res) { ...@@ -139,12 +163,19 @@ exports.epayIPN = function (req, res) {
); );
if (expected !== b.merchantToken) { if (expected !== b.merchantToken) {
console.error("epayIPN: INVALID_SIGNATURE for merTrxId=" + merTrxId + " resultCd=" + resultCd);
return res.status(400).json({ code: "99", data: "INVALID_SIGNATURE" }); 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"; var newStatus = resultCd === "00_000" ? "success" : "failed";
AdminTransaction.updateOne( AdminTransaction.updateOne(
{ merTrxId: merTrxId }, { merTrxId: merTrxId, status: "pending" },
{ {
status: newStatus, status: newStatus,
resultMsg: b.resultMsg || "", resultMsg: b.resultMsg || "",
...@@ -152,6 +183,7 @@ exports.epayIPN = function (req, res) { ...@@ -152,6 +183,7 @@ exports.epayIPN = function (req, res) {
}, },
function (updateErr) { function (updateErr) {
if (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(500).json({ code: "99", data: "DB_ERROR" });
} }
return res.status(200).json({ code: "00", data: "Success" }); return res.status(200).json({ code: "00", data: "Success" });
...@@ -170,6 +202,7 @@ exports.listTransactions = function (req, res) { ...@@ -170,6 +202,7 @@ exports.listTransactions = function (req, res) {
.limit(perPage) .limit(perPage)
.exec(function (err, list) { .exec(function (err, list) {
if (err) { if (err) {
console.error("listTransactions: DB error:", err.message);
return res.status(500).send("Lỗi cơ sở dữ liệu"); return res.status(500).send("Lỗi cơ sở dữ liệu");
} }
var transactions = list.map(function (tx) { var transactions = list.map(function (tx) {
...@@ -187,6 +220,10 @@ exports.listTransactions = function (req, res) { ...@@ -187,6 +220,10 @@ exports.listTransactions = function (req, res) {
res.render("admin/transactions-list", { res.render("admin/transactions-list", {
transactions: transactions, transactions: transactions,
page: page, page: page,
prevPage: page - 1,
nextPage: page + 1,
hasPrev: page > 1,
hasNext: transactions.length === perPage,
}); });
}); });
}; };
"use strict"; "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) { module.exports = function basicAuth(req, res, next) {
var config = require(__config_path + "/config"); 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 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 decoded = Buffer.from(token, "base64").toString("utf8");
var parts = decoded.split(":"); var sepIndex = decoded.indexOf(":");
var user = parts[0]; var user = sepIndex === -1 ? decoded : decoded.slice(0, sepIndex);
var password = parts[1]; 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(); return next();
} }
......
...@@ -4,7 +4,7 @@ var Schema = mongoose.Schema; ...@@ -4,7 +4,7 @@ var Schema = mongoose.Schema;
var AdminTransactionSchema = new 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 }, transCode: { type: String, required: true },
customerName: { type: String, required: true }, customerName: { type: String, required: true },
customerPhone: { type: String, required: true }, customerPhone: { type: String, required: true },
......
...@@ -12,29 +12,34 @@ ...@@ -12,29 +12,34 @@
<h2>Không tìm thấy giao dịch</h2> <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> <p>Đường dẫn thanh toán không hợp lệ hoặc đã hết hạn.</p>
{% else %} {% else %}
<h2>Thanh toán {{tx.amount}} VNĐ</h2> {% if alreadyPaid %}
<p>Khách hàng: {{tx.customerName}}</p> <h2>Giao dịch này đã được xử lý</h2>
<form id="megapayForm" name="megapayForm" method="POST"> <p>Trạng thái: {{tx.status}}</p>
<input type="hidden" name="merId" value="{{merId}}"> {% else %}
<input type="hidden" name="currency" value="VND"> <h2>Thanh toán {{tx.amount}} VNĐ</h2>
<input type="hidden" name="amount" value="{{tx.amount}}"> <p>Khách hàng: {{tx.customerName}}</p>
<input type="hidden" name="invoiceNo" value="{{tx.transCode}}"> <form id="megapayForm" name="megapayForm" method="POST">
<input type="hidden" name="merTrxId" value="{{tx.merTrxId}}"> <input type="hidden" name="merId" value="{{merId}}">
<input type="hidden" name="goodsNm" value="Thanh toan Hai Yen"> <input type="hidden" name="currency" value="VND">
<input type="hidden" name="payType" value="{{tx.payType}}"> <input type="hidden" name="amount" value="{{tx.amount}}">
<input type="hidden" name="description" value="Thanh toan don hang"> <input type="hidden" name="invoiceNo" value="{{tx.transCode}}">
<input type="hidden" name="callBackUrl" value="{{callBackUrl}}"> <input type="hidden" name="merTrxId" value="{{tx.merTrxId}}">
<input type="hidden" name="notiUrl" value="{{notiUrl}}"> <input type="hidden" name="goodsNm" value="Thanh toan Hai Yen">
<input type="hidden" name="reqDomain" value="{{reqDomain}}"> <input type="hidden" name="payType" value="{{tx.payType}}">
<input type="hidden" name="merchantToken" value="{{tx.merchantToken}}"> <input type="hidden" name="description" value="Thanh toan don hang">
<input type="hidden" name="timeStamp" value="{{tx.timeStamp}}"> <input type="hidden" name="callBackUrl" value="{{callBackUrl}}">
<input type="hidden" name="userLanguage" value="VN"> <input type="hidden" name="notiUrl" value="{{notiUrl}}">
<input type="hidden" name="windowColor" value="#ef5459"> <input type="hidden" name="reqDomain" value="{{reqDomain}}">
<input type="hidden" name="windowType" value=""> <input type="hidden" name="merchantToken" value="{{tx.merchantToken}}">
</form> <input type="hidden" name="timeStamp" value="{{tx.timeStamp}}">
<button onclick="openPayment(1, '{{domain}}')">Thanh toán</button> <input type="hidden" name="userLanguage" value="VN">
<script src="/js/jquery.min.js"></script> <input type="hidden" name="windowColor" value="#ef5459">
<script src="{{domain}}/pg_was/js/payment/layer/paymentClient.js"></script> <input type="hidden" name="windowType" value="">
</form>
<button onclick="openPayment(1, '{{domain}}')">Thanh toán</button>
<script src="/js/jquery.min.js"></script>
<script src="{{domain}}/pg_was/js/payment/layer/paymentClient.js"></script>
{% endif %}
{% endif %} {% endif %}
</body> </body>
</html> </html>
...@@ -4,13 +4,15 @@ ...@@ -4,13 +4,15 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>Lịch sử giao dịch</title> <title>Lịch sử giao dịch</title>
<style> <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; } table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; font-size: 14px; } th, td { border: 1px solid #ccc; padding: 8px; text-align: left; font-size: 14px; }
th { background: #f0f0f0; } th { background: #f0f0f0; }
.status-pending { color: #b8860b; } .status-pending { color: #b8860b; }
.status-success { color: #276b45; } .status-success { color: #276b45; }
.status-failed { color: #a3352a; } .status-failed { color: #a3352a; }
.pager { margin-top: 16px; }
.pager a { margin-right: 12px; }
</style> </style>
</head> </head>
<body> <body>
...@@ -20,10 +22,12 @@ ...@@ -20,10 +22,12 @@
<thead> <thead>
<tr> <tr>
<th>Thời gian</th> <th>Thời gian</th>
<th>Mã giao dịch</th>
<th>Khách hàng</th> <th>Khách hàng</th>
<th>SĐT</th> <th>SĐT</th>
<th>Số tiền</th> <th>Số tiền</th>
<th>Trạng thái</th> <th>Trạng thái</th>
<th>Ghi chú</th>
<th>Link thanh toán</th> <th>Link thanh toán</th>
</tr> </tr>
</thead> </thead>
...@@ -31,14 +35,21 @@ ...@@ -31,14 +35,21 @@
{% for tx in transactions %} {% for tx in transactions %}
<tr> <tr>
<td>{{tx.createdAt}}</td> <td>{{tx.createdAt}}</td>
<td>{{tx.merTrxId}}</td>
<td>{{tx.customerName}}</td> <td>{{tx.customerName}}</td>
<td>{{tx.customerPhone}}</td> <td>{{tx.customerPhone}}</td>
<td>{{tx.amount}}</td> <td>{{tx.amount}}</td>
<td class="status-{{tx.status}}">{{tx.status}}</td> <td class="status-{{tx.status}}">{{tx.status}}</td>
<td>{{tx.resultMsg}}</td>
<td><a href="{{tx.paymentUrl}}" target="_blank">Link</a></td> <td><a href="{{tx.paymentUrl}}" target="_blank">Link</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </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> </body>
</html> </html>
...@@ -12,9 +12,13 @@ var init = require('./config/init')(), ...@@ -12,9 +12,13 @@ var init = require('./config/init')(),
// Avoids DEPTH_ZERO_SELF_SIGNED_CERT error for self-signed certs // Avoids DEPTH_ZERO_SELF_SIGNED_CERT error for self-signed certs
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
mongoose.set('bufferCommands', false);
mongoose.connect(config.mongoUri, { mongoose.connect(config.mongoUri, {
useNewUrlParser: true, 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) { mongoose.connection.on('error', function (err) {
console.error('MongoDB connection error:', err.message); 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