Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
V
VCCI-News
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
Commits
330d67d3
Commit
330d67d3
authored
May 18, 2026
by
Lê Bảo Hồng Đức
☄
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix
parent
edbc9f87
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
99 additions
and
68 deletions
+99
-68
page.tsx
src/app/admin/base-config/page.tsx
+4
-4
page.tsx
src/app/admin/dashboard/page.tsx
+2
-2
page.tsx
src/app/admin/news/page.tsx
+12
-12
admin-auth-guard.tsx
src/components/shared/admin-auth-guard.tsx
+40
-15
admin-sidebar.tsx
src/components/shared/admin-sidebar.tsx
+28
-27
admin-auth.ts
src/lib/auth/admin-auth.ts
+4
-2
useAuthStore.ts
src/store/useAuthStore.ts
+9
-6
No files found.
src/app/admin/base-config/page.tsx
View file @
330d67d3
...
...
@@ -579,7 +579,7 @@ export default function AdminBaseConfigPage() {
value=
"social"
className=
"rounded-xl px-4 py-2.5 text-sm font-semibold text-slate-600 hover:text-[#063e8e] data-[state=active]:bg-white data-[state=active]:text-[#063e8e]"
>
M
?ng x? h?
i
M
ạng xã hộ
i
</
TabsTrigger
>
</
TabsList
>
...
...
@@ -1022,9 +1022,9 @@ export default function AdminBaseConfigPage() {
<
CardHeader
>
<
div
className=
"flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"
>
<
div
>
<
CardTitle
className=
"text-2xl text-[#163b73]"
>
M
?ng x? h?
i
</
CardTitle
>
<
CardTitle
className=
"text-2xl text-[#163b73]"
>
M
ạng xã hộ
i
</
CardTitle
>
<
CardDescription
className=
"mt-2 text-sm text-slate-600"
>
Qu
?n l? link m?ng x? h?i v? th? t? hi?n th? tr?
n website.
Qu
ản lý link mạng xã hội và thứ tự hiển thị trê
n website.
</
CardDescription
>
</
div
>
...
...
@@ -1034,7 +1034,7 @@ export default function AdminBaseConfigPage() {
className=
"rounded-xl bg-[#163b73] text-white hover:bg-[#163b73]/90"
>
<
Save
className=
"mr-2 h-4 w-4"
/>
Luu
m?ng x? h?i
Luu
cấu hình
</
Button
>
</
div
>
</
CardHeader
>
...
...
src/app/admin/dashboard/page.tsx
View file @
330d67d3
...
...
@@ -159,7 +159,7 @@ export default function AdminDashboardPage() {
()
=>
[
{
title
:
"Cấu hình chung"
,
description
:
"Logo, banner, chi nh
?nh li?n h? v? m?ng x? h?
i"
,
description
:
"Logo, banner, chi nh
ánh liên hệ và mạng xã hộ
i"
,
href
:
"/admin/base-config"
,
icon
:
Globe
,
},
...
...
@@ -313,7 +313,7 @@ export default function AdminDashboardPage() {
<
div
className=
"mt-2 text-2xl font-semibold text-[#163b73]"
>
{
activeBanners
.
length
}
</
div
>
</
div
>
<
div
className=
"rounded-[24px] border border-[#063e8e]/10 bg-white p-4"
>
<
div
className=
"text-xs uppercase tracking-[0.14em] text-slate-400"
>
M
?ng x? h?i hi?n th?
</
div
>
<
div
className=
"text-xs uppercase tracking-[0.14em] text-slate-400"
>
M
ạng xã hội hiển thị
</
div
>
<
div
className=
"mt-2 text-2xl font-semibold text-[#163b73]"
>
{
visibleSocials
.
length
}
</
div
>
</
div
>
</
div
>
...
...
src/app/admin/news/page.tsx
View file @
330d67d3
...
...
@@ -317,6 +317,12 @@ export default function AdminNewsPage() {
filters
.
push
(
`category.id==
${
categoryFilter
}
`
);
}
if
(
typeFilter
===
"tintuc"
)
{
filters
.
push
(
"type==news"
);
}
else
if
(
typeFilter
===
"baiviettrang"
)
{
filters
.
push
(
"type==page"
);
}
if
(
statusFilter
===
"visible"
)
{
filters
.
push
(
"is_hidden==false"
);
}
else
if
(
statusFilter
===
"hidden"
)
{
...
...
@@ -324,7 +330,7 @@ export default function AdminNewsPage() {
}
return
filters
.
join
(
","
);
},
[
categoryFilter
,
debouncedSearch
,
statusFilter
]);
},
[
categoryFilter
,
debouncedSearch
,
statusFilter
,
typeFilter
]);
const
load
=
React
.
useCallback
(
async
()
=>
{
setReady
(
false
);
...
...
@@ -387,14 +393,6 @@ export default function AdminNewsPage() {
);
},
[
headerItems
]);
const
filteredItems
=
React
.
useMemo
(()
=>
{
if
(
typeFilter
===
"all"
)
{
return
items
;
}
return
items
.
filter
((
item
)
=>
item
.
type
===
typeFilter
);
},
[
items
,
typeFilter
]);
const
stats
=
React
.
useMemo
(()
=>
{
return
[
{
...
...
@@ -452,7 +450,9 @@ export default function AdminNewsPage() {
actionLabel=
"Thêm bài viết"
actionIcon=
{
<
Plus
className=
"mr-2 h-4 w-4"
/>
}
onSearchChange=
{
setSearch
}
onActionClick=
{
()
=>
router
.
push
(
"/admin/news/new"
)
}
onActionClick=
{
()
=>
router
.
push
(
`/admin/news/new?returnTo=${encodeURIComponent(listPath)}`
)
}
filters=
{
<
div
className=
"flex flex-col gap-3 lg:flex-row lg:items-center"
>
<
Select
value=
{
typeFilter
}
onValueChange=
{
setTypeFilter
}
>
...
...
@@ -531,14 +531,14 @@ export default function AdminNewsPage() {
<
TableBody
>
{
!
ready
?
(
<
AdminNewsTableLoading
/>
)
:
filteredI
tems
.
length
===
0
?
(
)
:
i
tems
.
length
===
0
?
(
<
TableRow
>
<
TableCell
colSpan=
{
7
}
className=
"py-12 text-center text-sm text-gray-700"
>
Không có bài viết nào phù hợp.
</
TableCell
>
</
TableRow
>
)
:
(
filteredI
tems
.
map
((
item
,
index
)
=>
{
i
tems
.
map
((
item
,
index
)
=>
{
const
categoryNames
=
getDisplayCategoryNames
(
item
,
headerItems
);
const
primaryCategoryName
=
categoryNames
[
0
]
??
"
\
u2014"
;
const
extraCategoryCount
=
Math
.
max
(
categoryNames
.
length
-
1
,
0
);
...
...
src/components/shared/admin-auth-guard.tsx
View file @
330d67d3
...
...
@@ -75,43 +75,59 @@ export function AdminAuthGuard({ children }: { children: React.ReactNode }) {
const
accessTokenExpired
=
useAuthStore
((
state
)
=>
state
.
appAccessTokenExpired
);
const
refreshToken
=
useAuthStore
((
state
)
=>
state
.
appRefreshToken
);
const
isRefreshing
=
useAuthStore
((
state
)
=>
state
.
appIsRefreshing
);
const
[
isRestoringSession
,
setIsRestoringSession
]
=
useState
(
false
);
const
[
authCheckState
,
setAuthCheckState
]
=
useState
<
"idle"
|
"checking"
|
"ready"
>
(
"idle"
);
const
redirectParam
=
encodeURIComponent
(
pathname
||
"/admin"
);
useEffect
(()
=>
{
if
(
!
hasHydrated
||
pathname
===
LOGIN_PATH
)
return
;
if
(
pathname
===
LOGIN_PATH
)
{
setAuthCheckState
(
"ready"
);
return
;
}
if
(
!
hasHydrated
)
{
setAuthCheckState
(
"idle"
);
return
;
}
let
cancelled
=
false
;
const
restoreSession
=
async
()
=>
{
const
needsRefresh
=
Boolean
(
setAuthCheckState
(
"checking"
);
const
hasValidAccessToken
=
Boolean
(
accessToken
&&
accessTokenExpired
&&
accessTokenExpired
<=
Date
.
now
()
&&
refreshToken
,
isLoggedIn
&&
(
!
accessTokenExpired
||
accessTokenExpired
>
Date
.
now
()),
);
if
(
accessToken
&&
isLoggedIn
&&
!
needsRefresh
)
return
;
if
(
hasValidAccessToken
)
{
if
(
!
cancelled
)
{
setAuthCheckState
(
"ready"
);
}
return
;
}
if
(
!
refreshToken
)
{
router
.
replace
(
`
${
LOGIN_PATH
}
?redirect=
${
encodeURIComponent
(
pathname
)}
`
);
if
(
!
cancelled
)
{
setAuthCheckState
(
"ready"
);
router
.
replace
(
`
${
LOGIN_PATH
}
?redirect=
${
redirectParam
}
`
);
}
return
;
}
setIsRestoringSession
(
true
);
try
{
const
nextToken
=
await
ensureValidAdminAccessToken
();
if
(
!
nextToken
&&
!
cancelled
)
{
router
.
replace
(
`
${
LOGIN_PATH
}
?redirect=
${
encodeURIComponent
(
pathname
)
}
`
);
router
.
replace
(
`
${
LOGIN_PATH
}
?redirect=
${
redirectParam
}
`
);
}
}
catch
{
if
(
!
cancelled
)
{
router
.
replace
(
`
${
LOGIN_PATH
}
?redirect=
${
encodeURIComponent
(
pathname
)
}
`
);
router
.
replace
(
`
${
LOGIN_PATH
}
?redirect=
${
redirectParam
}
`
);
}
}
finally
{
if
(
!
cancelled
)
{
set
IsRestoringSession
(
false
);
set
AuthCheckState
(
"ready"
);
}
}
};
...
...
@@ -121,13 +137,22 @@ export function AdminAuthGuard({ children }: { children: React.ReactNode }) {
return
()
=>
{
cancelled
=
true
;
};
},
[
accessToken
,
accessTokenExpired
,
hasHydrated
,
isLoggedIn
,
pathname
,
refreshToken
,
router
]);
},
[
accessToken
,
accessTokenExpired
,
hasHydrated
,
isLoggedIn
,
pathname
,
redirectParam
,
refreshToken
,
router
,
]);
if
(
pathname
===
LOGIN_PATH
)
{
return
<>
{
children
}
</>;
}
if
(
!
hasHydrated
||
isRefreshing
||
isRestoringSession
)
{
if
(
!
hasHydrated
||
isRefreshing
||
authCheckState
!==
"ready"
)
{
return
<
AdminAuthLoadingScreen
/>;
}
...
...
src/components/shared/admin-sidebar.tsx
View file @
330d67d3
...
...
@@ -35,33 +35,34 @@ const navigation: NavItem[] = [
{
name
:
'Quản lý bài viết'
,
href
:
'/admin/news'
,
icon
:
Newspaper
},
{
name
:
'Quản lý tag tìm kiếm'
,
href
:
'/admin/tags'
,
icon
:
Tags
},
{
name
:
'Quản lý video'
,
href
:
'/admin/videos'
,
icon
:
Video
},
{
name
:
'Quản lý hội viên'
,
icon
:
Users
,
children
:
[
{
name
:
'Danh sách hội viên'
,
href
:
'/admin/members'
},
{
name
:
'Quản lý lĩnh vực'
,
href
:
'/admin/members/fields'
},
{
name
:
'Quản lý khu vực'
,
href
:
'/admin/members/regions'
},
],
},
{
name
:
'Quản lý liên hệ'
,
icon
:
Mail
,
children
:
[
{
name
:
'Quản lý Email đăng ký nhận thông tin'
,
href
:
'/admin/contact-management/newsletter-emails'
,
},
{
name
:
'Quản lý Đơn liên hệ'
,
href
:
'/admin/contact-management/contact-requests'
,
},
{
name
:
'Quản lý Đơn đăng ký hội viên'
,
href
:
'/admin/contact-management/membership-applications'
,
},
],
},
// {
// name: 'Quản lý hội viên',
// icon: Users,
// children: [
// { name: 'Danh sách hội viên', href: '/admin/members' },
// { name: 'Quản lý lĩnh vực', href: '/admin/members/fields' },
// { name: 'Quản lý khu vực', href: '/admin/members/regions' },
// ],
// },
// {
// name: 'Quản lý liên hệ',
// icon: Mail,
// children: [
// {
// name: 'Quản lý Email đăng ký nhận thông tin',
// href: '/admin/contact-management/newsletter-emails',
// },
// {
// name: 'Quản lý Đơn liên hệ',
// href: '/admin/contact-management/contact-requests',
// },
// {
// name: 'Quản lý Đơn đăng ký hội viên',
// href: '/admin/contact-management/membership-applications',
// },
// ],
// },
{
name
:
'Quản lý Email đăng ký'
,
href
:
'/admin/contact-management/newsletter-emails'
,
icon
:
Mail
},
{
name
:
'Quản lý ảnh'
,
href
:
'/admin/media'
,
icon
:
ImagePlus
},
];
...
...
src/lib/auth/admin-auth.ts
View file @
330d67d3
...
...
@@ -7,7 +7,7 @@ import useAuthStore, {
}
from
"@/store/useAuthStore"
;
const
AUTH_BASE_URL
=
`
${
process
.
env
.
NEXT_PUBLIC_BACKEND_HOST
}
/api/v1.0/auth`
;
const
SESSION_EXPIRED_MESSAGE
=
"Phi
?n dang nh?p d? h?t h?n. Vui l?ng dang nh?p l?
i."
;
const
SESSION_EXPIRED_MESSAGE
=
"Phi
ên đăng nhập đã hết hạn. Vui lòng đăng nhập lạ
i."
;
interface
AuthEnvelope
<
T
>
{
message
?:
string
|
null
;
...
...
@@ -50,6 +50,7 @@ interface RefreshResponseData {
interface
AuthRequestOptions
extends
RequestInit
{
skipAuthHeader
?:
boolean
;
authToken
?:
string
|
null
;
}
type
AuthFailureReason
=
"missing_refresh_token"
|
"refresh_failed"
;
...
...
@@ -114,7 +115,7 @@ async function requestAuth<T>(
headers
.
set
(
"Content-Type"
,
"application/json"
);
if
(
!
init
?.
skipAuthHeader
)
{
const
token
=
useAuthStore
.
getState
().
appAccessToken
;
const
token
=
init
?.
authToken
??
useAuthStore
.
getState
().
appAccessToken
;
if
(
token
&&
!
headers
.
has
(
"Authorization"
))
{
headers
.
set
(
"Authorization"
,
`Bearer
${
token
}
`
);
}
...
...
@@ -178,6 +179,7 @@ export async function loginAdmin(email: string, password: string) {
const
me
=
await
requestAuth
<
MeResponseData
>
(
"/me"
,
{
method
:
"GET"
,
authToken
:
payload
.
access_token
,
}).
catch
(()
=>
payload
.
user
??
null
);
const
normalizedUser
=
normalizeUser
(
me
??
payload
.
user
);
...
...
src/store/useAuthStore.ts
View file @
330d67d3
...
...
@@ -49,7 +49,7 @@ export interface AuthStoreStateType {
remember
:
boolean
;
}
|
null
;
_hasHydrated
:
boolean
;
setHasHydrated
:
(
state
:
AuthStoreStateType
)
=>
void
;
setHasHydrated
:
(
hasHydrated
?:
boolean
)
=>
void
;
setAppIsLoggedIn
:
(
isLoggedIn
:
boolean
)
=>
void
;
setAuthSession
:
(
payload
:
AuthSessionPayload
)
=>
void
;
updateAccessToken
:
(
payload
:
AuthRefreshPayload
)
=>
void
;
...
...
@@ -94,9 +94,9 @@ const useAuthStore = create<AuthStoreStateType>()(
persist
(
(
set
,
get
)
=>
({
...
baseState
,
setHasHydrated
:
(
state
:
AuthStoreStateTyp
e
)
=>
setHasHydrated
:
(
hasHydrated
=
tru
e
)
=>
set
(()
=>
({
_hasHydrated
:
state
!=
undefin
ed
,
_hasHydrated
:
hasHydrat
ed
,
})),
setAppIsLoggedIn
:
(
isLoggedIn
:
boolean
)
=>
set
(()
=>
({
...
...
@@ -184,10 +184,13 @@ const useAuthStore = create<AuthStoreStateType>()(
appSessionExpiredNotified
:
state
.
appSessionExpiredNotified
,
appUserRemember
:
state
.
appUserRemember
,
}),
onRehydrateStorage
:
()
=>
{
onRehydrateStorage
:
(
state
)
=>
{
return
(
state
:
AuthStoreStateType
|
undefined
,
error
:
unknown
)
=>
{
if
(
error
||
state
==
undefined
)
return
;
state
.
setHasHydrated
(
state
);
if
(
error
)
{
useAuthStore
.
persist
.
clearStorage
();
}
(
state
??
useAuthStore
.
getState
()).
setHasHydrated
(
true
);
};
},
},
...
...
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