Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
H
haiyen-payment
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Giang Tran
haiyen-payment
Commits
557e8965
Commit
557e8965
authored
Aug 28, 2026
by
tdgiang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fix critical/important findings from final whole-branch review
parent
8f6e76b9
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
114 additions
and
38 deletions
+114
-38
admin.server.controller.js
app/controllers/admin.server.controller.js
+44
-7
basicAuth.js
app/middlewares/basicAuth.js
+24
-5
AdminTransaction.js
app/models/AdminTransaction.js
+1
-1
pay.server.view.html
app/views/admin/pay.server.view.html
+28
-23
transactions-list.server.view.html
app/views/admin/transactions-list.server.view.html
+12
-1
server.js
server.js
+5
-1
No files found.
app/controllers/admin.server.controller.js
View file @
557e8965
...
...
@@ -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
a
mount
=
req
.
body
.
amount
;
var
rawA
mount
=
req
.
body
.
amount
;
if
(
!
customerName
||
!
customerPhone
||
!
customerAddress
||
!
a
mount
)
{
if
(
!
customerName
||
!
customerPhone
||
!
customerAddress
||
!
rawA
mount
)
{
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
,
});
});
};
app/middlewares/basicAuth.js
View file @
557e8965
"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
();
}
...
...
app/models/AdminTransaction.js
View file @
557e8965
...
...
@@ -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
},
...
...
app/views/admin/pay.server.view.html
View file @
557e8965
...
...
@@ -12,29 +12,34 @@
<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 %}
<h2>
Thanh toán {{tx.amount}} VNĐ
</h2>
<p>
Khách hàng: {{tx.customerName}}
</p>
<form
id=
"megapayForm"
name=
"megapayForm"
method=
"POST"
>
<input
type=
"hidden"
name=
"merId"
value=
"{{merId}}"
>
<input
type=
"hidden"
name=
"currency"
value=
"VND"
>
<input
type=
"hidden"
name=
"amount"
value=
"{{tx.amount}}"
>
<input
type=
"hidden"
name=
"invoiceNo"
value=
"{{tx.transCode}}"
>
<input
type=
"hidden"
name=
"merTrxId"
value=
"{{tx.merTrxId}}"
>
<input
type=
"hidden"
name=
"goodsNm"
value=
"Thanh toan Hai Yen"
>
<input
type=
"hidden"
name=
"payType"
value=
"{{tx.payType}}"
>
<input
type=
"hidden"
name=
"description"
value=
"Thanh toan don hang"
>
<input
type=
"hidden"
name=
"callBackUrl"
value=
"{{callBackUrl}}"
>
<input
type=
"hidden"
name=
"notiUrl"
value=
"{{notiUrl}}"
>
<input
type=
"hidden"
name=
"reqDomain"
value=
"{{reqDomain}}"
>
<input
type=
"hidden"
name=
"merchantToken"
value=
"{{tx.merchantToken}}"
>
<input
type=
"hidden"
name=
"timeStamp"
value=
"{{tx.timeStamp}}"
>
<input
type=
"hidden"
name=
"userLanguage"
value=
"VN"
>
<input
type=
"hidden"
name=
"windowColor"
value=
"#ef5459"
>
<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>
{% 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"
>
<input
type=
"hidden"
name=
"merId"
value=
"{{merId}}"
>
<input
type=
"hidden"
name=
"currency"
value=
"VND"
>
<input
type=
"hidden"
name=
"amount"
value=
"{{tx.amount}}"
>
<input
type=
"hidden"
name=
"invoiceNo"
value=
"{{tx.transCode}}"
>
<input
type=
"hidden"
name=
"merTrxId"
value=
"{{tx.merTrxId}}"
>
<input
type=
"hidden"
name=
"goodsNm"
value=
"Thanh toan Hai Yen"
>
<input
type=
"hidden"
name=
"payType"
value=
"{{tx.payType}}"
>
<input
type=
"hidden"
name=
"description"
value=
"Thanh toan don hang"
>
<input
type=
"hidden"
name=
"callBackUrl"
value=
"{{callBackUrl}}"
>
<input
type=
"hidden"
name=
"notiUrl"
value=
"{{notiUrl}}"
>
<input
type=
"hidden"
name=
"reqDomain"
value=
"{{reqDomain}}"
>
<input
type=
"hidden"
name=
"merchantToken"
value=
"{{tx.merchantToken}}"
>
<input
type=
"hidden"
name=
"timeStamp"
value=
"{{tx.timeStamp}}"
>
<input
type=
"hidden"
name=
"userLanguage"
value=
"VN"
>
<input
type=
"hidden"
name=
"windowColor"
value=
"#ef5459"
>
<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 %}
</body>
</html>
app/views/admin/transactions-list.server.view.html
View file @
557e8965
...
...
@@ -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
:
9
00px
;
margin
:
40px
auto
;
}
body
{
font-family
:
Arial
,
sans-serif
;
max-width
:
10
00px
;
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}}"
>
«
Trang trước
</a>
{% endif %}
Trang {{page}}
{% if hasNext %}
<a
href=
"/admin/transactions?page={{nextPage}}"
>
Trang sau
»
</a>
{% endif %}
</p>
</body>
</html>
server.js
View file @
557e8965
...
...
@@ -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
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment