Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
V
vcci-news-frontend
Project
Project
Details
Activity
Releases
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
Văn Hoàng
vcci-news-frontend
Commits
5368db18
Commit
5368db18
authored
Oct 27, 2026
by
Phạm Quang Bảo
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: login
parent
6455da6b
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
94 additions
and
60 deletions
+94
-60
custom-client.ts
src/api/vcci-news/mutator/custom-client.ts
+5
-4
layout.tsx
src/app/admin/login/layout.tsx
+52
-0
page.tsx
src/app/admin/login/page.tsx
+33
-54
admin-auth-guard.tsx
src/components/layout/admin/admin-auth-guard.tsx
+4
-2
No files found.
src/api/vcci-news/mutator/custom-client.ts
View file @
5368db18
...
...
@@ -92,12 +92,13 @@ instance.interceptors.response.use(
useUserStore
.
getState
().
clearUser
();
if
(
typeof
window
!==
"undefined"
)
{
const
currentPath
=
`
${
window
.
location
.
pathname
}${
window
.
location
.
search
}
`
;
const
redirect
=
currentPath
.
startsWith
(
"/admin"
)
&&
currentPath
!==
"/admin/login"
const
isLoginPage
=
window
.
location
.
pathname
===
"/admin/login"
;
if
(
!
isLoginPage
)
{
const
redirect
=
currentPath
.
startsWith
(
"/admin"
)
?
`?redirect=
${
encodeURIComponent
(
currentPath
)}
`
:
""
;
window
.
location
.
replace
(
`/admin/login
${
redirect
}
`
);
window
.
location
.
replace
(
`/admin/login
${
redirect
}
`
);
}
}
return
Promise
.
reject
(
error
);
}
...
...
src/app/admin/login/layout.tsx
0 → 100644
View file @
5368db18
"use client"
;
import
{
useEffect
}
from
"react"
;
import
{
useRouter
}
from
"next/navigation"
;
import
useAuthStore
from
"@/store/useAuthStore"
;
import
useUserStore
from
"@/store/useUserStore"
;
const
DEFAULT_ADMIN_PATH
=
"/admin"
;
function
normalizeRedirectPath
(
redirect
:
string
|
null
)
{
if
(
!
redirect
)
return
DEFAULT_ADMIN_PATH
;
try
{
const
url
=
new
URL
(
redirect
,
"http://localhost"
);
const
pathname
=
url
.
pathname
;
const
isAdminPath
=
pathname
===
"/admin"
||
pathname
.
startsWith
(
"/admin/"
);
const
isAuthPath
=
pathname
===
"/admin/login"
||
pathname
.
startsWith
(
"/admin/login/"
)
||
pathname
===
"/admin/change-password"
;
if
(
!
isAdminPath
||
isAuthPath
)
return
DEFAULT_ADMIN_PATH
;
return
`
${
pathname
}${
url
.
search
}${
url
.
hash
}
`
;
}
catch
{
return
DEFAULT_ADMIN_PATH
;
}
}
export
default
function
LoginLayout
({
children
}:
{
children
:
React
.
ReactNode
})
{
const
router
=
useRouter
();
const
userHasHydrated
=
useUserStore
((
state
)
=>
state
.
_hasHydrated
);
const
appUser
=
useUserStore
((
state
)
=>
state
.
appUser
);
const
authHasHydrated
=
useAuthStore
((
state
)
=>
state
.
_hasHydrated
);
const
isLoggedIn
=
useAuthStore
((
state
)
=>
state
.
appIsLoggedIn
);
useEffect
(()
=>
{
if
(
!
userHasHydrated
||
!
authHasHydrated
||
!
isLoggedIn
||
!
appUser
)
return
;
const
redirect
=
normalizeRedirectPath
(
new
URLSearchParams
(
window
.
location
.
search
).
get
(
"redirect"
),
);
if
(
appUser
.
must_change_password
)
{
router
.
replace
(
"/admin/change-password"
);
}
else
{
router
.
replace
(
redirect
);
}
},
[
appUser
,
authHasHydrated
,
isLoggedIn
,
router
,
userHasHydrated
]);
return
<>
{
children
}
</>;
}
src/app/admin/login/page.tsx
View file @
5368db18
"use client"
;
import
{
FormEvent
,
Suspense
,
useEffect
,
useMemo
,
useState
}
from
"react"
;
import
{
useRouter
,
useSearchParams
}
from
"next/navigation"
;
import
{
FormEvent
,
Suspense
,
useEffect
,
useState
}
from
"react"
;
import
{
ArrowLeft
,
CheckCircle2
,
...
...
@@ -34,25 +33,11 @@ type ErrorResponse = {
};
};
const
DEFAULT_REDIRECT
=
"/admin"
;
const
authFieldClassName
=
"h-11 rounded-xl border-[#063e8e]/15 bg-white text-gray-700 placeholder:text-gray-400 shadow-sm focus-visible:ring-[#063e8e]/30"
;
const
authButtonClassName
=
"h-11 rounded-xl bg-[#063e8e] text-white shadow-[0_12px_24px_rgba(6,62,142,0.16)] hover:bg-[#052f6c]"
;
function
normalizeRedirectPath
(
redirect
:
string
|
null
)
{
if
(
!
redirect
||
!
redirect
.
startsWith
(
"/admin"
)
||
redirect
===
"/admin/login"
||
redirect
===
"/admin/change-password"
)
{
return
DEFAULT_REDIRECT
;
}
return
redirect
;
}
function
getAuthErrorMessage
(
error
:
unknown
,
fallback
:
string
)
{
const
apiError
=
error
as
{
response
?:
{
...
...
@@ -73,12 +58,12 @@ function normalizeUser(
user
:
any
,
mustChangePassword
:
boolean
,
):
AuthenticatedAdminUser
|
null
{
if
(
!
user
?.
id
||
!
user
.
email
||
!
user
.
username
)
return
null
;
if
(
!
user
?.
id
)
return
null
;
return
{
id
:
user
.
id
,
email
:
user
.
email
,
username
:
user
.
username
,
id
:
String
(
user
.
id
)
,
email
:
String
(
user
.
email
??
""
)
,
username
:
String
(
user
.
username
??
user
.
email
??
""
)
,
first_name
:
user
.
first_name
??
null
,
last_name
:
user
.
last_name
??
null
,
roles
:
Array
.
isArray
(
user
.
roles
)
...
...
@@ -116,15 +101,7 @@ function InlineMessage({ type, message }: {
}
function
AdminLoginPageContent
()
{
const
router
=
useRouter
();
const
searchParams
=
useSearchParams
();
const
redirect
=
useMemo
(
()
=>
normalizeRedirectPath
(
searchParams
.
get
(
"redirect"
)),
[
searchParams
],
);
const
hasHydrated
=
useAuthStore
((
state
)
=>
state
.
_hasHydrated
);
const
isLoggedIn
=
useAuthStore
((
state
)
=>
state
.
appIsLoggedIn
);
const
rememberState
=
useAuthStore
((
state
)
=>
state
.
appUserRemember
);
const
setAuthSession
=
useAuthStore
((
state
)
=>
state
.
setAuthSession
);
const
setAppUser
=
useUserStore
((
state
)
=>
state
.
setAppUser
);
...
...
@@ -154,17 +131,6 @@ function AdminLoginPageContent() {
setRemember
(
true
);
},
[
rememberState
]);
useEffect
(()
=>
{
if
(
!
hasHydrated
||
!
isLoggedIn
)
return
;
const
currentUser
=
useUserStore
.
getState
().
appUser
;
if
(
currentUser
?.
must_change_password
)
{
router
.
replace
(
"/admin/change-password"
);
}
else
{
router
.
replace
(
redirect
);
}
},
[
hasHydrated
,
isLoggedIn
,
redirect
,
router
]);
const
{
mutate
:
login
,
isPending
:
loginLoading
}
=
usePostApiV10AuthLogin
({
mutation
:
{
onSuccess
:
async
(
res
:
any
)
=>
{
...
...
@@ -174,9 +140,6 @@ function AdminLoginPageContent() {
setLoginError
(
"Thiếu dữ liệu phiên đăng nhập từ API."
);
return
;
}
const
mustChangePassword
=
loginData
.
must_change_password
===
true
;
setAuthSession
({
accessToken
:
loginData
.
access_token
,
refreshToken
:
loginData
.
refresh_token
,
...
...
@@ -189,6 +152,8 @@ function AdminLoginPageContent() {
remember
,
);
const
mustChangePassword
=
loginData
.
must_change_password
===
true
;
try
{
const
profile
=
await
getProfile
();
const
profileData
=
...
...
@@ -196,25 +161,36 @@ function AdminLoginPageContent() {
(
profile
.
data
as
any
)?.
data
?.
responseData
;
const
normalizedUser
=
normalizeUser
(
profileData
,
mustChangePassword
);
if
(
normalizedUser
)
{
setAppUser
(
normalizedUser
);
}
else
if
(
loginData
.
user
)
{
if
(
normalizedUser
)
setAppUser
(
normalizedUser
);
else
if
(
loginData
.
user
)
{
const
fallbackUser
=
normalizeUser
(
loginData
.
user
,
mustChangePassword
);
if
(
fallbackUser
)
setAppUser
(
fallbackUser
);
}
if
(
mustChangePassword
)
{
toast
.
success
(
"Đăng nhập thành công. Vui lòng đổi mật khẩu để tiếp tục."
);
}
else
{
toast
.
success
(
"Đăng nhập quản trị thành công"
);
}
}
catch
{
if
(
loginData
.
user
)
{
const
fallbackUser
=
normalizeUser
(
loginData
.
user
,
mustChangePassword
);
const
stillLoggedIn
=
useAuthStore
.
getState
().
appIsLoggedIn
;
let
fallbackUser
:
AuthenticatedAdminUser
|
null
=
null
;
if
(
stillLoggedIn
&&
loginData
.
user
)
{
fallbackUser
=
normalizeUser
(
loginData
.
user
,
mustChangePassword
);
if
(
fallbackUser
)
setAppUser
(
fallbackUser
);
}
}
if
(
mustChangePassword
)
{
toast
.
success
(
"Đăng nhập thành công. Vui lòng đổi mật khẩu để tiếp tục."
);
router
.
replace
(
"/admin/change-password"
);
}
else
{
toast
.
success
(
"Đăng nhập quản trị thành công"
);
router
.
replace
(
redirect
);
if
(
!
fallbackUser
)
{
useAuthStore
.
getState
().
resetStore
();
useUserStore
.
getState
().
clearUser
();
setLoginError
(
"Không tải được thông tin tài khoản. Vui lòng thử đăng nhập lại."
,
);
return
;
}
}
},
onError
:
(
err
:
any
)
=>
{
...
...
@@ -228,6 +204,9 @@ function AdminLoginPageContent() {
const
handleLogin
=
async
(
event
:
FormEvent
<
HTMLFormElement
>
)
=>
{
event
.
preventDefault
();
setLoginError
(
null
);
// Do not let a stale profile from a previous session trigger the login
// layout redirect before the new profile request has completed.
useUserStore
.
getState
().
clearUser
();
login
({
data
:
{
email
:
email
.
trim
(),
...
...
src/components/layout/admin/admin-auth-guard.tsx
View file @
5368db18
...
...
@@ -79,10 +79,11 @@ export function AdminAuthGuard({ children }: { children: React.ReactNode }) {
useEffect
(()
=>
{
if
(
hasHydrated
&&
!
isLoggedIn
&&
pathname
!==
LOGIN_PATH
)
{
const
currentPath
=
`
${
window
.
location
.
pathname
}${
window
.
location
.
search
}
`
;
const
isLoginPage
=
window
.
location
.
pathname
===
LOGIN_PATH
;
const
isChangePasswordPage
=
currentPath
.
startsWith
(
CHANGE_PASSWORD_PATH
);
const
redirect
=
currentPath
.
startsWith
(
"/admin"
)
&&
currentPath
!==
LOGIN_PATH
&&
!
isLoginPage
&&
!
isChangePasswordPage
?
`?redirect=
${
encodeURIComponent
(
currentPath
)}
`
:
""
;
...
...
@@ -126,10 +127,11 @@ export function useAdminAuthStatus() {
useEffect
(()
=>
{
if
(
hasHydrated
&&
!
isLoggedIn
&&
pathname
!==
LOGIN_PATH
)
{
const
currentPath
=
`
${
window
.
location
.
pathname
}${
window
.
location
.
search
}
`
;
const
isLoginPage
=
window
.
location
.
pathname
===
LOGIN_PATH
;
const
isChangePasswordPage
=
currentPath
.
startsWith
(
CHANGE_PASSWORD_PATH
);
const
redirect
=
currentPath
.
startsWith
(
"/admin"
)
&&
currentPath
!==
LOGIN_PATH
&&
!
isLoginPage
&&
!
isChangePasswordPage
?
`?redirect=
${
encodeURIComponent
(
currentPath
)}
`
:
""
;
...
...
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