Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Nguyễn Lương Duy
landingpage_spc
Commits
5be553be
Commit
5be553be
authored
Mar 19, 2026
by
DuyNL
Browse files
update code
parent
47a23d19
Changes
6
Show whitespace changes
Inline
Side-by-side
app/api/otp/generate/route.ts
View file @
5be553be
...
@@ -6,6 +6,7 @@ import {
...
@@ -6,6 +6,7 @@ import {
verifyCsrfToken
,
verifyCsrfToken
,
}
from
"
@/lib/csrf
"
;
}
from
"
@/lib/csrf
"
;
import
{
apiRequest
}
from
"
@/lib/api
"
;
import
{
apiRequest
}
from
"
@/lib/api
"
;
import
{
getPhoneCookieConfig
}
from
"
@/lib/session
"
;
import
{
buildUnauthorizedResponse
,
hasVerifiedSession
}
from
"
@/lib/session-guard
"
;
import
{
buildUnauthorizedResponse
,
hasVerifiedSession
}
from
"
@/lib/session-guard
"
;
function
buildCsrfResponse
(
message
:
string
,
status
:
number
)
{
function
buildCsrfResponse
(
message
:
string
,
status
:
number
)
{
...
@@ -28,23 +29,45 @@ export async function POST(request: NextRequest) {
...
@@ -28,23 +29,45 @@ export async function POST(request: NextRequest) {
const
csrfCookie
=
request
.
cookies
.
get
(
getCsrfCookieConfig
().
name
)?.
value
;
const
csrfCookie
=
request
.
cookies
.
get
(
getCsrfCookieConfig
().
name
)?.
value
;
const
csrfHeader
=
request
.
headers
.
get
(
csrfHeaderName
);
const
csrfHeader
=
request
.
headers
.
get
(
csrfHeaderName
);
const
accessToken
=
request
.
headers
.
get
(
"
access_token
"
)
||
request
.
headers
.
get
(
"
x-access-token
"
)
||
request
.
headers
.
get
(
"
access-token
"
)
||
""
;
const
maKH
=
request
.
headers
.
get
(
"
ma_khang
"
)
||
request
.
headers
.
get
(
"
Ma_khang
"
)
||
""
;
if
(
!
verifyCsrfToken
(
csrfCookie
)
||
!
verifyCsrfToken
(
csrfHeader
)
||
csrfCookie
!==
csrfHeader
)
{
if
(
!
verifyCsrfToken
(
csrfCookie
)
||
!
verifyCsrfToken
(
csrfHeader
)
||
csrfCookie
!==
csrfHeader
)
{
return
buildCsrfResponse
(
"
CSRF token không hợp lệ.
"
,
403
);
return
buildCsrfResponse
(
"
CSRF token không hợp lệ.
"
,
403
);
}
}
try
{
try
{
const
phoneForLog
=
request
.
cookies
.
get
(
getPhoneCookieConfig
().
name
)?.
value
||
""
;
const
{
ok
,
payload
,
status
}
=
await
apiRequest
({
const
{
ok
,
payload
,
status
}
=
await
apiRequest
({
url
:
"
/posts
"
,
//point/api/otp/generate
url
:
process
.
env
.
NEXT_PUBLIC_BE_API_OTP_GENERATE
??
""
,
method
:
"
POST
"
,
method
:
"
POST
"
,
body
:
JSON
.
stringify
({
access_token
:
accessToken
,
ma_khang
:
maKH
,
phone
:
phoneForLog
}),
phoneForLog
,
});
});
const
refreshedCsrfToken
=
createCsrfToken
();
const
refreshedCsrfToken
=
createCsrfToken
();
const
nextPayload
=
const
nextPayload
=
payload
&&
typeof
payload
===
"
object
"
payload
&&
typeof
payload
===
"
object
"
?
{
...
payload
,
csrfToken
:
refreshedCsrfToken
}
?
{
...
payload
,
csrfToken
:
refreshedCsrfToken
}
:
{
data
:
payload
??
null
,
csrfToken
:
refreshedCsrfToken
};
:
{
data
:
payload
??
null
,
csrfToken
:
refreshedCsrfToken
};
const
isBusinessSuccess
=
ok
&&
typeof
nextPayload
===
"
object
"
&&
nextPayload
!==
null
&&
"
status
"
in
nextPayload
&&
nextPayload
.
status
===
"
success
"
;
const
nextResponse
=
NextResponse
.
json
(
nextPayload
,
{
const
nextResponse
=
NextResponse
.
json
(
nextPayload
,
{
status
:
ok
?
status
:
status
||
502
,
status
:
isBusinessSuccess
?
status
:
status
>=
400
?
status
:
400
,
});
});
const
cookie
=
getCsrfCookieConfig
();
const
cookie
=
getCsrfCookieConfig
();
...
...
app/api/otp/verify/route.ts
View file @
5be553be
...
@@ -6,6 +6,7 @@ import {
...
@@ -6,6 +6,7 @@ import {
verifyCsrfToken
,
verifyCsrfToken
,
}
from
"
@/lib/csrf
"
;
}
from
"
@/lib/csrf
"
;
import
{
apiRequest
}
from
"
@/lib/api
"
;
import
{
apiRequest
}
from
"
@/lib/api
"
;
import
{
getPhoneCookieConfig
}
from
"
@/lib/session
"
;
import
{
buildUnauthorizedResponse
,
hasVerifiedSession
}
from
"
@/lib/session-guard
"
;
import
{
buildUnauthorizedResponse
,
hasVerifiedSession
}
from
"
@/lib/session-guard
"
;
function
buildCsrfResponse
(
message
:
string
,
status
:
number
)
{
function
buildCsrfResponse
(
message
:
string
,
status
:
number
)
{
...
@@ -46,10 +47,26 @@ export async function POST(request: NextRequest) {
...
@@ -46,10 +47,26 @@ export async function POST(request: NextRequest) {
}
}
try
{
try
{
const
accessToken
=
request
.
headers
.
get
(
"
access_token
"
)
||
request
.
headers
.
get
(
"
x-access-token
"
)
||
request
.
headers
.
get
(
"
access-token
"
)
||
""
;
const
maKH
=
request
.
headers
.
get
(
"
ma_khang
"
)
||
request
.
headers
.
get
(
"
Ma_khang
"
)
||
""
;
const
phoneForLog
=
request
.
cookies
.
get
(
getPhoneCookieConfig
().
name
)?.
value
||
""
;
const
{
ok
,
payload
,
status
}
=
await
apiRequest
({
const
{
ok
,
payload
,
status
}
=
await
apiRequest
({
url
:
"
/epoint/api/otp/verify
"
,
url
:
process
.
env
.
NEXT_PUBLIC_BE_API_OTP_VERIFY
??
"
"
,
method
:
"
POST
"
,
method
:
"
POST
"
,
body
:
JSON
.
stringify
({
otp
:
body
.
otp
}),
body
:
JSON
.
stringify
({
access_token
:
accessToken
,
ma_khang
:
maKH
,
phone
:
phoneForLog
,
id_epoint
:
123
,
otp
:
body
.
otp
}),
phoneForLog
,
});
});
const
refreshedCsrfToken
=
createCsrfToken
();
const
refreshedCsrfToken
=
createCsrfToken
();
const
nextPayload
=
const
nextPayload
=
...
...
app/page-client.tsx
View file @
5be553be
...
@@ -59,7 +59,7 @@ async function generateOtp(csrfToken: string) {
...
@@ -59,7 +59,7 @@ async function generateOtp(csrfToken: string) {
if
(
!
response
.
ok
)
{
if
(
!
response
.
ok
)
{
const
error
=
new
Error
(
const
error
=
new
Error
(
payload
.
message
||
"
Kh
o
ng th
e
g
u
i OTP. Vui l
o
ng th
u
l
a
i.
"
,
payload
.
message
||
"
Kh
ô
ng th
ể
g
ử
i OTP. Vui l
ò
ng th
ử
l
ạ
i.
"
,
)
as
Error
&
{
)
as
Error
&
{
csrfToken
?:
string
;
csrfToken
?:
string
;
};
};
...
@@ -86,7 +86,7 @@ async function verifyOtp(otp: string, csrfToken: string) {
...
@@ -86,7 +86,7 @@ async function verifyOtp(otp: string, csrfToken: string) {
if
(
!
response
.
ok
)
{
if
(
!
response
.
ok
)
{
const
error
=
new
Error
(
const
error
=
new
Error
(
payload
.
message
||
"
X
a
c th
u
c OTP th
a
t b
a
i. Vui l
o
ng th
u
l
a
i.
"
,
payload
.
message
||
"
X
á
c th
ự
c OTP th
ấ
t b
ạ
i. Vui l
ò
ng th
ử
l
ạ
i.
"
,
)
as
Error
&
{
)
as
Error
&
{
csrfToken
?:
string
;
csrfToken
?:
string
;
};
};
...
@@ -130,7 +130,7 @@ export default function PageClient({
...
@@ -130,7 +130,7 @@ export default function PageClient({
const
generateOtpMutation
=
useMutation
({
const
generateOtpMutation
=
useMutation
({
mutationFn
:
()
=>
{
mutationFn
:
()
=>
{
if
(
!
csrfToken
)
{
if
(
!
csrfToken
)
{
throw
new
Error
(
"
CSRF token ch
u
a s
a
n s
a
ng. Vui l
o
ng th
u
l
a
i.
"
);
throw
new
Error
(
"
CSRF token ch
ư
a s
ẵ
n s
à
ng. Vui l
ò
ng th
ử
l
ạ
i.
"
);
}
}
return
generateOtp
(
csrfToken
);
return
generateOtp
(
csrfToken
);
...
@@ -159,7 +159,7 @@ export default function PageClient({
...
@@ -159,7 +159,7 @@ export default function PageClient({
const
verifyOtpMutation
=
useMutation
({
const
verifyOtpMutation
=
useMutation
({
mutationFn
:
()
=>
{
mutationFn
:
()
=>
{
if
(
!
csrfToken
)
{
if
(
!
csrfToken
)
{
throw
new
Error
(
"
CSRF token ch
u
a s
a
n s
a
ng. Vui l
o
ng th
u
l
a
i.
"
);
throw
new
Error
(
"
CSRF token ch
ư
a s
ẵ
n s
à
ng. Vui l
ò
ng th
ử
l
ạ
i.
"
);
}
}
return
verifyOtp
(
otp
,
csrfToken
);
return
verifyOtp
(
otp
,
csrfToken
);
...
@@ -205,7 +205,7 @@ export default function PageClient({
...
@@ -205,7 +205,7 @@ export default function PageClient({
const
handleOtpSubmit
=
()
=>
{
const
handleOtpSubmit
=
()
=>
{
if
(
otp
.
length
!==
OTP_LENGTH
)
{
if
(
otp
.
length
!==
OTP_LENGTH
)
{
setError
(
"
Vui l
o
ng nh
a
p
du
6 s
o
OTP.
"
);
setError
(
"
Vui l
ò
ng nh
ậ
p
đủ
6 s
ố
OTP.
"
);
return
;
return
;
}
}
...
@@ -304,7 +304,7 @@ export default function PageClient({
...
@@ -304,7 +304,7 @@ export default function PageClient({
<
div
className
=
"absolute inset-0 bg-slate-900/35"
/>
<
div
className
=
"absolute inset-0 bg-slate-900/35"
/>
<
div
className
=
"relative z-10 w-full max-w-[340px] rounded-[24px] bg-white px-5 py-6 shadow-[0_20px_50px_rgba(15,23,42,0.2)]"
>
<
div
className
=
"relative z-10 w-full max-w-[340px] rounded-[24px] bg-white px-5 py-6 shadow-[0_20px_50px_rgba(15,23,42,0.2)]"
>
<
div
className
=
"space-y-4 text-center"
>
<
div
className
=
"space-y-4 text-center"
>
<
h2
className
=
"text-2xl font-bold text-slate-900"
>
Th
o
ng b
a
o
</
h2
>
<
h2
className
=
"text-2xl font-bold text-slate-900"
>
Th
ô
ng b
á
o
</
h2
>
<
p
className
=
"text-[15px] leading-6 text-slate-600"
>
<
p
className
=
"text-[15px] leading-6 text-slate-600"
>
{
initialVerifyError
}
{
initialVerifyError
}
</
p
>
</
p
>
...
@@ -313,7 +313,7 @@ export default function PageClient({
...
@@ -313,7 +313,7 @@ export default function PageClient({
onClick
=
{
()
=>
setVerifyErrorPopupOpen
(
false
)
}
onClick
=
{
()
=>
setVerifyErrorPopupOpen
(
false
)
}
className
=
"w-full rounded-2xl bg-brand-blue px-4 py-3 text-base font-bold text-white"
className
=
"w-full rounded-2xl bg-brand-blue px-4 py-3 text-base font-bold text-white"
>
>
Do
ng
Đó
ng
</
button
>
</
button
>
</
div
>
</
div
>
</
div
>
</
div
>
...
...
lib/api.ts
View file @
5be553be
...
@@ -13,6 +13,7 @@ type ApiRequestOptions = {
...
@@ -13,6 +13,7 @@ type ApiRequestOptions = {
headers
?:
HeadersInit
;
headers
?:
HeadersInit
;
body
?:
BodyInit
|
null
;
body
?:
BodyInit
|
null
;
baseUrl
?:
string
;
baseUrl
?:
string
;
phoneForLog
?:
string
;
};
};
type
ApiRequestResult
<
T
>
=
{
type
ApiRequestResult
<
T
>
=
{
...
@@ -37,9 +38,10 @@ export async function apiRequest<T extends Record<string, unknown> | ApiError>({
...
@@ -37,9 +38,10 @@ export async function apiRequest<T extends Record<string, unknown> | ApiError>({
headers
,
headers
,
body
=
null
,
body
=
null
,
baseUrl
,
baseUrl
,
phoneForLog
=
""
,
}:
ApiRequestOptions
):
Promise
<
ApiRequestResult
<
T
>>
{
}:
ApiRequestOptions
):
Promise
<
ApiRequestResult
<
T
>>
{
try
{
try
{
const
apiSPC
=
baseUrl
||
process
.
env
.
NEXT_PUBLIC_
SPC
_API
;
const
apiSPC
=
baseUrl
||
process
.
env
.
NEXT_PUBLIC_
BE
_API
;
if
(
!
apiSPC
)
{
if
(
!
apiSPC
)
{
throw
new
Error
(
"
Thiếu cấu hình domain.
"
);
throw
new
Error
(
"
Thiếu cấu hình domain.
"
);
...
@@ -59,8 +61,9 @@ export async function apiRequest<T extends Record<string, unknown> | ApiError>({
...
@@ -59,8 +61,9 @@ export async function apiRequest<T extends Record<string, unknown> | ApiError>({
const
vnTime
=
new
Date
().
toLocaleString
(
"
vi-VN
"
,
{
const
vnTime
=
new
Date
().
toLocaleString
(
"
vi-VN
"
,
{
timeZone
:
"
Asia/Ho_Chi_Minh
"
,
timeZone
:
"
Asia/Ho_Chi_Minh
"
,
});
});
const
phoneLog
=
phoneForLog
?
` phone=
${
phoneForLog
}
`
:
""
;
console
.
log
(
`
${
vnTime
}
: [apiRequest]
${
method
}
${
url
}
:`
,
payload
);
console
.
log
(
`
${
vnTime
}
: [apiRequest]
${
method
}
${
url
}
${
phoneLog
}
:`
,
payload
);
return
{
return
{
ok
:
response
.
ok
,
ok
:
response
.
ok
,
...
@@ -71,8 +74,9 @@ export async function apiRequest<T extends Record<string, unknown> | ApiError>({
...
@@ -71,8 +74,9 @@ export async function apiRequest<T extends Record<string, unknown> | ApiError>({
const
vnTime
=
new
Date
().
toLocaleString
(
"
vi-VN
"
,
{
const
vnTime
=
new
Date
().
toLocaleString
(
"
vi-VN
"
,
{
timeZone
:
"
Asia/Ho_Chi_Minh
"
,
timeZone
:
"
Asia/Ho_Chi_Minh
"
,
});
});
const
phoneLog
=
phoneForLog
?
` phone=
${
phoneForLog
}
`
:
""
;
console
.
error
(
`
${
vnTime
}
: [apiRequest]
${
method
}
${
url
}
failed: `
,
error
);
console
.
error
(
`
${
vnTime
}
: [apiRequest]
${
method
}
${
url
}
${
phoneLog
}
failed: `
,
error
);
if
(
error
instanceof
Error
)
{
if
(
error
instanceof
Error
)
{
throw
error
;
throw
error
;
...
...
lib/session.ts
View file @
5be553be
const
SESSION_COOKIE_NAME
=
"
epoint_session
"
;
const
SESSION_COOKIE_NAME
=
"
epoint_session
"
;
const
PHONE_COOKIE_NAME
=
"
epoint_phone
"
;
const
SESSION_TTL_MS
=
24
*
60
*
60
*
1000
;
const
SESSION_TTL_MS
=
24
*
60
*
60
*
1000
;
type
SessionPayload
=
{
type
SessionPayload
=
{
...
@@ -125,6 +126,18 @@ export function getSessionCookieConfig() {
...
@@ -125,6 +126,18 @@ export function getSessionCookieConfig() {
};
};
}
}
export
function
getPhoneCookieConfig
()
{
return
{
name
:
PHONE_COOKIE_NAME
,
options
:
{
httpOnly
:
true
,
sameSite
:
"
lax
"
as
const
,
secure
:
process
.
env
.
NODE_ENV
===
"
production
"
,
path
:
"
/
"
,
},
};
}
export
function
isSuccessfulSession
(
export
function
isSuccessfulSession
(
session
:
Awaited
<
ReturnType
<
typeof
verifySessionToken
>>
|
null
,
session
:
Awaited
<
ReturnType
<
typeof
verifySessionToken
>>
|
null
,
)
{
)
{
...
...
middleware.ts
View file @
5be553be
import
{
NextRequest
,
NextResponse
}
from
"
next/server
"
;
import
{
NextRequest
,
NextResponse
}
from
"
next/server
"
;
import
{
v
erifyTokenForWebView
}
from
"
@/lib/api
"
;
import
{
apiRequest
,
V
erifyTokenForWebView
Response
}
from
"
@/lib/api
"
;
import
{
import
{
createSessionToken
,
createSessionToken
,
getPhoneCookieConfig
,
getSessionCookieConfig
,
getSessionCookieConfig
,
isSuccessfulSession
,
isSuccessfulSession
,
verifySessionToken
,
verifySessionToken
,
...
@@ -26,6 +27,32 @@ function encodeHeaderValue(value: string) {
...
@@ -26,6 +27,32 @@ function encodeHeaderValue(value: string) {
return
encodeURIComponent
(
value
);
return
encodeURIComponent
(
value
);
}
}
function
extractPhoneFromPayload
(
payload
:
Record
<
string
,
unknown
>
):
string
{
const
candidateKeys
=
[
"
phone
"
,
"
customer_phone
"
,
"
phone_number
"
,
"
mobile
"
,
"
msisdn
"
,
];
for
(
const
key
of
candidateKeys
)
{
const
value
=
payload
[
key
];
if
(
typeof
value
===
"
string
"
&&
value
.
trim
())
{
return
value
.
trim
();
}
}
const
nestedData
=
payload
.
data
;
if
(
nestedData
&&
typeof
nestedData
===
"
object
"
&&
!
Array
.
isArray
(
nestedData
))
{
return
extractPhoneFromPayload
(
nestedData
as
Record
<
string
,
unknown
>
);
}
return
""
;
}
export
async
function
middleware
(
request
:
NextRequest
)
{
export
async
function
middleware
(
request
:
NextRequest
)
{
if
(
request
.
nextUrl
.
pathname
!==
"
/
"
)
{
if
(
request
.
nextUrl
.
pathname
!==
"
/
"
)
{
return
NextResponse
.
next
();
return
NextResponse
.
next
();
...
@@ -33,11 +60,12 @@ export async function middleware(request: NextRequest) {
...
@@ -33,11 +60,12 @@ export async function middleware(request: NextRequest) {
const
requestHeaders
=
new
Headers
(
request
.
headers
);
const
requestHeaders
=
new
Headers
(
request
.
headers
);
const
sessionCookieConfig
=
getSessionCookieConfig
();
const
sessionCookieConfig
=
getSessionCookieConfig
();
const
phoneCookieConfig
=
getPhoneCookieConfig
();
const
phoneFromCookie
=
request
.
cookies
.
get
(
phoneCookieConfig
.
name
)?.
value
||
""
;
//check session to call api
//check session to call api
const
sessionCookie
=
request
.
cookies
.
get
(
getSessionCookieConfig
().
name
)?.
value
;
const
sessionCookie
=
request
.
cookies
.
get
(
getSessionCookieConfig
().
name
)?.
value
;
const
session
=
await
verifySessionToken
(
sessionCookie
);
const
session
=
await
verifySessionToken
(
sessionCookie
);
const
buildNextResponse
=
()
=>
const
buildNextResponse
=
()
=>
NextResponse
.
next
({
NextResponse
.
next
({
request
:
{
request
:
{
...
@@ -65,14 +93,26 @@ export async function middleware(request: NextRequest) {
...
@@ -65,14 +93,26 @@ export async function middleware(request: NextRequest) {
);
);
const
response
=
buildNextResponse
();
const
response
=
buildNextResponse
();
response
.
cookies
.
delete
(
sessionCookieConfig
.
name
);
response
.
cookies
.
delete
(
sessionCookieConfig
.
name
);
response
.
cookies
.
delete
(
phoneCookieConfig
.
name
);
return
response
;
return
response
;
}
}
try
{
try
{
const
{
ok
,
payload
}
=
await
verifyTokenForWebView
(
accessToken
);
const
{
ok
,
payload
}
=
await
apiRequest
<
VerifyTokenForWebViewResponse
>
({
baseUrl
:
process
.
env
.
NEXT_PUBLIC_BE_API
,
url
:
process
.
env
.
NEXT_PUBLIC_BE_API_VERIFY
??
"
/20984/gup2start/rest/accountVerifyTokenForWebView/1.0.0
"
,
method
:
"
POST
"
,
body
:
JSON
.
stringify
({
access_token
:
accessToken
,
}),
phoneForLog
:
phoneFromCookie
,
});
if
(
ok
&&
payload
.
status
===
"
success
"
)
{
if
(
ok
&&
payload
.
status
===
"
success
"
)
{
const
phone
=
extractPhoneFromPayload
(
payload
as
Record
<
string
,
unknown
>
);
requestHeaders
.
set
(
"
x-session-valid
"
,
"
1
"
);
requestHeaders
.
set
(
"
x-session-valid
"
,
"
1
"
);
const
response
=
buildNextResponse
();
const
response
=
buildNextResponse
();
response
.
cookies
.
set
(
response
.
cookies
.
set
(
...
@@ -83,6 +123,11 @@ export async function middleware(request: NextRequest) {
...
@@ -83,6 +123,11 @@ export async function middleware(request: NextRequest) {
}),
}),
sessionCookieConfig
.
options
,
sessionCookieConfig
.
options
,
);
);
if
(
phone
)
{
response
.
cookies
.
set
(
phoneCookieConfig
.
name
,
phone
,
phoneCookieConfig
.
options
);
}
else
{
response
.
cookies
.
delete
(
phoneCookieConfig
.
name
);
}
return
response
;
return
response
;
}
}
...
@@ -102,6 +147,7 @@ export async function middleware(request: NextRequest) {
...
@@ -102,6 +147,7 @@ export async function middleware(request: NextRequest) {
}),
}),
sessionCookieConfig
.
options
,
sessionCookieConfig
.
options
,
);
);
response
.
cookies
.
delete
(
phoneCookieConfig
.
name
);
return
response
;
return
response
;
}
catch
(
error
)
{
}
catch
(
error
)
{
...
@@ -121,6 +167,7 @@ export async function middleware(request: NextRequest) {
...
@@ -121,6 +167,7 @@ export async function middleware(request: NextRequest) {
}),
}),
sessionCookieConfig
.
options
,
sessionCookieConfig
.
options
,
);
);
response
.
cookies
.
delete
(
phoneCookieConfig
.
name
);
return
response
;
return
response
;
}
}
...
...
Write
Preview
Supports
Markdown
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