From 49b99289bb9b5d68d919f3c47baa763100add1bd Mon Sep 17 00:00:00 2001 From: ducanh Date: Mon, 20 Apr 2026 00:47:41 +0700 Subject: [PATCH] pig update: decentralization, format date time --- middleware.ts | 67 ++++ .../(tables)/applications-tables/page.tsx | 8 +- .../(tables)/user-table/page.tsx | 365 +++++++++--------- .../profile/applications/page.tsx | 108 +++--- .../(error-pages)/error-403/page.tsx | 53 +++ src/components/auth/ProtectedRoute.tsx | 62 +++ src/components/auth/SignInForm.tsx | 3 + src/components/header/UserDropdown.tsx | 8 +- src/config/routes.config.ts | 144 +++++++ src/hooks/useAuth.ts | 66 ++++ src/hooks/useRestoreUserData.ts | 19 + src/layout/AppSidebar.tsx | 39 +- src/lib/cookieStorage.ts | 37 ++ src/store/StoreProvider.tsx | 14 +- src/store/features/userSlice.ts | 20 +- src/store/store.ts | 6 +- 16 files changed, 779 insertions(+), 240 deletions(-) create mode 100644 middleware.ts create mode 100644 src/app/(full-width-pages)/(error-pages)/error-403/page.tsx create mode 100644 src/components/auth/ProtectedRoute.tsx create mode 100644 src/config/routes.config.ts create mode 100644 src/hooks/useAuth.ts create mode 100644 src/hooks/useRestoreUserData.ts create mode 100644 src/lib/cookieStorage.ts diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..4037863 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,67 @@ +import { NextRequest, NextResponse } from "next/server" +import { PUBLIC_ROUTES, canAccessRoute, UserRole } from "./src/config/routes.config" + +/** + * Middleware để kiểm tra authentication và authorization + * Chạy TRƯỚC khi render page + */ +export async function middleware(request: NextRequest) { + const { pathname } = request.nextUrl + + // 1. Kiểm tra nếu là public route + if (PUBLIC_ROUTES.includes(pathname)) { + // Nếu user đã login, không cho vào signin/signup + const userDataCookie = request.cookies.get("userDataRedux") + if (userDataCookie && (pathname === "/signin" || pathname === "/signup")) { + return NextResponse.redirect(new URL("/", request.url)) + } + return NextResponse.next() + } + + // 2. Kiểm tra token (cookies) + const token = request.cookies.get("token") || request.cookies.get("access_token") + const userDataCookie = request.cookies.get("userDataRedux") + + // 3. Nếu không có token, redirect về signin + if (!token) { + const signinUrl = new URL("/signin", request.url) + signinUrl.searchParams.set("from", pathname) + return NextResponse.redirect(signinUrl) + } + + // 4. Kiểm tra role-based access + if (userDataCookie) { + try { + const userData = JSON.parse(userDataCookie.value) + const userRoles: UserRole[] = userData.roles?.map((r: any) => r.name) || [] + + // Kiểm tra user có quyền truy cập route này không + if (!canAccessRoute(userRoles, pathname)) { + // Redirect về dashboard hoặc 403 page + return NextResponse.redirect(new URL("/error-403", request.url)) + } + } catch (error) { + console.error("Error parsing user data in middleware:", error) + // Nếu lỗi parse, vẫn cho qua (để tránh infinite redirect) + return NextResponse.next() + } + } + + return NextResponse.next() +} + +/** + * Cấu hình matcher - middleware chỉ chạy cho những routes này + */ +export const config = { + matcher: [ + /* + * Chạy middleware cho tất cả paths ngoại trừ: + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * - public folder + */ + "/((?!_next/static|_next/image|favicon.ico|public).*)", + ], +} diff --git a/src/app/(admin)/(others-pages)/(tables)/applications-tables/page.tsx b/src/app/(admin)/(others-pages)/(tables)/applications-tables/page.tsx index 38a2d8e..bf12559 100644 --- a/src/app/(admin)/(others-pages)/(tables)/applications-tables/page.tsx +++ b/src/app/(admin)/(others-pages)/(tables)/applications-tables/page.tsx @@ -5,6 +5,7 @@ import ComponentCard from "@/components/common/ComponentCard"; import PageBreadcrumb from "@/components/common/PageBreadCrumb"; import Pagination from "@/components/tables/Pagination"; import Swal from "sweetalert2"; +import { ProtectedRoute } from "@/components/auth/ProtectedRoute"; import ApplicationTable, { AppSortColumn, @@ -190,8 +191,9 @@ export default function HistorianApplicationPage() { console.log(tableData) // console.log("Pagination info:", pagination); return ( -
- + +
+
} > - {/* Cập nhật Grid để chứa đủ các ô filter */}
@@ -354,5 +355,6 @@ export default function HistorianApplicationPage() { onRefresh={fetchApplications} />
+ ); } diff --git a/src/app/(admin)/(others-pages)/(tables)/user-table/page.tsx b/src/app/(admin)/(others-pages)/(tables)/user-table/page.tsx index 28659ee..5b557a1 100644 --- a/src/app/(admin)/(others-pages)/(tables)/user-table/page.tsx +++ b/src/app/(admin)/(others-pages)/(tables)/user-table/page.tsx @@ -15,6 +15,7 @@ import { import { useEffect, useState, useCallback } from "react"; import Pagination from "@/components/tables/Pagination"; import { LIMIT_ITEM_TABLE } from "../../../../../../constant"; +import { ProtectedRoute } from "@/components/auth/ProtectedRoute"; export type SortColumn = "created_at" | "updated_at" | "display_name" | "email"; @@ -32,7 +33,9 @@ const formatDateTimeToISO = ( export default function UserTable() { const [page, setPage] = useState(1); - const [limitInput, setLimitInput] = useState(LIMIT_ITEM_TABLE.toString()); + const [limitInput, setLimitInput] = useState( + LIMIT_ITEM_TABLE.toString(), + ); const [selectedRole, setSelectedRole] = useState(""); const [roles, setRoles] = useState<{ id: string; name: string }[]>([]); @@ -143,7 +146,6 @@ export default function UserTable() { role_ids: selectedRole ? [selectedRole] : undefined, }; - // Thêm format ngày giờ vào payload const createdFrom = formatDateTimeToISO( debouncedParams.fromDate, debouncedParams.fromTime, @@ -185,7 +187,7 @@ export default function UserTable() { }; const pagination = tableData?.pagination; - + // console.log(pagination); const handleOpenDetail = (user: fullDataUser) => { @@ -251,192 +253,197 @@ export default function UserTable() { }; return ( -
- -
- - +
+ +
+ - + + + + } + > +
+
+ + setSearchTerm(e.target.value)} + className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" /> - - - } - > -
-
- - setSearchTerm(e.target.value)} - className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ setFromDate(e.target.value)} + className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" + /> + setFromTime(e.target.value)} + className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" + /> +
+
+ +
+ +
+ setToDate(e.target.value)} + className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" + /> + setToTime(e.target.value)} + className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" + /> +
+
+ +
+ + setLimitInput(e.target.value)} + className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" + /> +
+
+ + + +
+ {loading && ( +
+
+
+ )} + + setSelectedRole(role)} />
-
- - -
+
+

+ Hiển thị trang {pagination?.current_page || 1} /{" "} + {pagination?.total_pages || 1} ({pagination?.total_records || 0}{" "} + kết quả) +

-
- - -
- - {/* Từ ngày */} -
- -
- setFromDate(e.target.value)} - className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" + {pagination && pagination.total_pages > 1 && ( + setPage(newPage)} /> - setFromTime(e.target.value)} - className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" - /> -
+ )}
- {/* Đến ngày */} -
- -
- setToDate(e.target.value)} - className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" - /> - setToTime(e.target.value)} - className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" - /> -
-
- - {/* Limit */} -
- - setLimitInput(e.target.value)} - className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" - /> -
-
-
- - -
- {loading && ( -
-
-
- )} - - setSelectedRole(role)} + setIsModalOpen(false)} + user={selectedUser} + onChangeRole={handleOpenRoleModal} + onDelete={(u) => { + handleDelete(u); + setIsModalOpen(false); + }} + onRestore={(u) => { + handleRestore(u); + setIsModalOpen(false); + }} /> -
-
-

- Hiển thị trang {pagination?.current_page || 1} /{" "} - {pagination?.total_pages || 1} ({pagination?.total_records || 0}{" "} - kết quả) -

- - {pagination && pagination.total_pages > 1 && ( - setPage(newPage)} - /> - )} -
- - setIsModalOpen(false)} - user={selectedUser} - onChangeRole={handleOpenRoleModal} - onDelete={(u) => { - handleDelete(u); - setIsModalOpen(false); - }} - onRestore={(u) => { - handleRestore(u); - setIsModalOpen(false); - }} - /> - - setIsRoleModalOpen(false)} - user={roleUser} - onSuccess={fetchUsers} - /> -
+ setIsRoleModalOpen(false)} + user={roleUser} + onSuccess={fetchUsers} + /> + +
-
+ ); -} \ No newline at end of file +} diff --git a/src/app/(admin)/(others-pages)/profile/applications/page.tsx b/src/app/(admin)/(others-pages)/profile/applications/page.tsx index 1b20d8d..a9dd032 100644 --- a/src/app/(admin)/(others-pages)/profile/applications/page.tsx +++ b/src/app/(admin)/(others-pages)/profile/applications/page.tsx @@ -38,6 +38,18 @@ export default function ApplicationDetailPage() { const config = statusConfig[application.status] || statusConfig.PENDING; + const formatDate = (dateString: string | null | undefined) => { + if (!dateString) return "-"; + const date = new Date(dateString); + return date.toLocaleDateString("vi-VN", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + }; + const isImageFile = (file: any) => { const isImageMime = file.mime_type?.startsWith("image/"); const isImageExt = /\.(jpg|jpeg|png|webp|gif)$/i.test(file.storage_key); @@ -214,55 +226,57 @@ export default function ApplicationDetailPage() {
)} - {application?.reviewer && ( -
- + {application?.reviewer && ( +
+ -
-
-
- {application?.reviewer?.avatar_url ? ( - {application.reviewer.display_name - ) : ( -
- {application?.reviewer?.display_name?.charAt(0) || "R"} +
+
+
+ {application?.reviewer?.avatar_url ? ( + {application.reviewer.display_name + ) : ( +
+ {application?.reviewer?.display_name?.charAt(0) || "R"} +
+ )} + +
+

+ {application?.reviewer?.display_name} +

+

+ {application?.reviewer?.email} +

+
+
+ + {formatDate(application?.reviewed_at)} + +
+ +
+
+ {application?.review_note ? ( +

{application.review_note}

+ ) : ( +

+ Không có ghi chú bổ sung. +

+ )} +
+
- )} - -
-

- {application?.reviewer?.display_name} -

-

- {application?.reviewer?.email} -

-
-
- - {application?.reviewed_at} - -
- -
-
- {application?.review_note ? ( -

{application.review_note}

- ) : ( -

Không có ghi chú bổ sung.

- )} -
-
-
-
-)} +
+ )}
+ +
+

+ 403 - ACCESS DENIED +

+ + 403 + 403 + +

+ You do not have permission to access this resource. Please contact your administrator if you believe this is a mistake. +

+ + + Back to Home Page + +
+

+ © {new Date().getFullYear()} - Admin Dashboard +

+
+ ); +} diff --git a/src/components/auth/ProtectedRoute.tsx b/src/components/auth/ProtectedRoute.tsx new file mode 100644 index 0000000..1ac545a --- /dev/null +++ b/src/components/auth/ProtectedRoute.tsx @@ -0,0 +1,62 @@ +"use client" + +import { ReactNode, useEffect } from "react" +import { useRouter } from "next/navigation" +import { useAuth } from "@/hooks/useAuth" +import { UserRole } from "@/config/routes.config" + +interface ProtectedRouteProps { + children: ReactNode + requiredRoles?: UserRole[] + fallback?: ReactNode +} + +/** + * Component để protect routes dựa trên role + * Sử dụng ở client-side trong layouts hoặc pages + */ +export const ProtectedRoute: React.FC = ({ + children, + requiredRoles = [], + fallback, +}) => { + const router = useRouter() + const { isAuthenticated, userRoles, hasAnyRole } = useAuth() + + useEffect(() => { + if (!isAuthenticated) { + router.replace("/signin") + } + }, [isAuthenticated, router]) + + if (!isAuthenticated) { + return null + } + + if (requiredRoles.length > 0 && !hasAnyRole(requiredRoles)) { + if (fallback) { + return <>{fallback} + } + router.replace("/error-403") + return null + } + + return <>{children} +} + +/** + * Wrapper để render UI dựa trên role + */ +export const RoleGate: React.FC<{ + requiredRoles: UserRole[] + children: ReactNode + fallback?: ReactNode +}> = ({ requiredRoles, children, fallback }) => { + const { hasAnyRole } = useAuth() + + if (!hasAnyRole(requiredRoles)) { + return <>{fallback || null} + } + + return <>{children} +} diff --git a/src/components/auth/SignInForm.tsx b/src/components/auth/SignInForm.tsx index bf11c12..714ac94 100644 --- a/src/components/auth/SignInForm.tsx +++ b/src/components/auth/SignInForm.tsx @@ -12,6 +12,7 @@ import { API, HOME_URL } from "../../../api"; import { setUserData } from "@/store/features/userSlice"; import { useDispatch } from "react-redux"; import { useRouter } from "next/navigation"; +import { saveUserToCookie } from "@/lib/cookieStorage"; export default function SignInForm() { const router = useRouter(); @@ -73,7 +74,9 @@ export default function SignInForm() { // console.log("Current User Data:", data); if (data?.data) { + // Lưu user data vào Redux và cookies dispatch(setUserData(data.data)); + saveUserToCookie(data.data); router.push("/"); } } else { diff --git a/src/components/header/UserDropdown.tsx b/src/components/header/UserDropdown.tsx index 59832e7..97abb7f 100644 --- a/src/components/header/UserDropdown.tsx +++ b/src/components/header/UserDropdown.tsx @@ -4,14 +4,11 @@ import Link from "next/link"; import React, { useEffect, useState } from "react"; import { Dropdown } from "../ui/dropdown/Dropdown"; import { DropdownItem } from "../ui/dropdown/DropdownItem"; -import { fullDataUser } from "@/interface/admin"; import { UserMetaCardProps } from "@/interface/user"; import { apiGetCurrentUser, apiLogout } from "@/service/auth"; -import { useRouter } from "next/navigation"; +import { removeUserFromCookie } from "@/lib/cookieStorage"; export default function UserDropdown() { - const router = useRouter(); - const [isOpen, setIsOpen] = useState(false); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); @@ -47,6 +44,9 @@ export default function UserDropdown() { } catch (error) { console.error("Logout failed", error); } finally { + // Xóa user data từ cookies + removeUserFromCookie(); + localStorage.clear(); sessionStorage.clear(); diff --git a/src/config/routes.config.ts b/src/config/routes.config.ts new file mode 100644 index 0000000..5c36d08 --- /dev/null +++ b/src/config/routes.config.ts @@ -0,0 +1,144 @@ +/** + * Role-Based Access Control Configuration + * Định nghĩa routes theo role và quyền truy cập + */ + +export type UserRole = "ADMIN" | "MOD" | "HISTORIAN" | "USER" + +export interface RouteConfig { + path: string + allowedRoles: UserRole[] + label?: string +} + +/** + * Public routes - không cần authentication + */ +export const PUBLIC_ROUTES = [ + "/signin", + "/signup", + "/reset-password", + "/error-403", + "/error-404", + "/error-500", +] + + +export const PROTECTED_ROUTES: RouteConfig[] = [ + // Dashboard + { + path: "/", + allowedRoles: ["ADMIN", "MOD", "HISTORIAN", "USER"], + label: "Dashboard", + }, + + // Admin/MOD + Historian routes + { + path: "/user-table", + allowedRoles: ["ADMIN", "MOD"], + label: "User Management", + }, + { + path: "/applications-tables", + allowedRoles: ["ADMIN", "MOD"], + label: "Applications", + }, + + // All authenticated users + { + path: "/profile", + allowedRoles: ["ADMIN", "MOD", "HISTORIAN", "USER"], + label: "User Profile", + }, + { + path: "/calendar", + allowedRoles: ["ADMIN", "MOD", "HISTORIAN", "USER"], + label: "Calendar", + }, + { + path: "/line-chart", + allowedRoles: ["ADMIN", "MOD", "HISTORIAN", "USER"], + label: "Line Chart", + }, + { + path: "/bar-chart", + allowedRoles: ["ADMIN", "MOD", "HISTORIAN", "USER"], + label: "Bar Chart", + }, +] + +/** + * Kiểm tra user role có được phép truy cập route không + */ +export const canAccessRoute = ( + userRoles: UserRole[] | undefined, + routePath: string +): boolean => { + // Nếu không có role, không được truy cập + if (!userRoles || userRoles.length === 0) { + return false + } + + // Public routes không cần check + if (PUBLIC_ROUTES.includes(routePath)) { + return true + } + + // Tìm route config theo độ dài path giảm dần để tránh route '/' khớp với tất cả + const routeConfig = PROTECTED_ROUTES + .slice() + .sort((a, b) => b.path.length - a.path.length) + .find( + (route) => + routePath === route.path || + (route.path !== "/" && routePath.startsWith(route.path + "/")) + ) + + // Nếu route không được định nghĩa, cho phép (mặc định) + if (!routeConfig) { + return true + } + + // Check xem user có role được phép không + return userRoles.some((role) => routeConfig.allowedRoles.includes(role)) +} + +/** + * Lấy danh sách routes cho user role + */ +export const getAvailableRoutes = (userRoles: UserRole[] | undefined): RouteConfig[] => { + if (!userRoles || userRoles.length === 0) { + return [] + } + + return PROTECTED_ROUTES.filter((route) => + userRoles.some((role) => route.allowedRoles.includes(role)) + ) +} + +/** + * Kiểm tra user có role nào không + */ +export const hasRole = (userRoles: UserRole[] | undefined, role: UserRole): boolean => { + return userRoles?.includes(role) ?? false +} + +/** + * Kiểm tra user có ít nhất một trong các role không + */ +export const hasAnyRole = ( + userRoles: UserRole[] | undefined, + roles: UserRole[] +): boolean => { + return userRoles?.some((role) => roles.includes(role)) ?? false +} + +/** + * Kiểm tra user có tất cả các role không + */ +export const hasAllRoles = ( + userRoles: UserRole[] | undefined, + roles: UserRole[] +): boolean => { + return roles.every((role) => userRoles?.includes(role)) +} diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts new file mode 100644 index 0000000..57ebee5 --- /dev/null +++ b/src/hooks/useAuth.ts @@ -0,0 +1,66 @@ +import { useAppSelector } from '@/store/store'; + +import { + canAccessRoute, + hasRole, + hasAnyRole, + hasAllRoles, + getAvailableRoutes, + UserRole, +} from "@/config/routes.config" + +/** + * Hook để kiểm tra authentication và authorization + */ +export const useAuth = () => { + const user = useAppSelector((state) => state.user.data) + const isAuthenticated = useAppSelector((state) => state.user.isAuthenticated) + + const userRoles = user?.roles?.map((r) => r.name) as UserRole[] | undefined + + return { + user, + isAuthenticated, + userRoles, + + /** + * Kiểm tra user có role cụ thể không + */ + hasRole: (role: UserRole) => hasRole(userRoles, role), + + /** + * Kiểm tra user có ít nhất một trong các role không + */ + hasAnyRole: (roles: UserRole[]) => hasAnyRole(userRoles, roles), + + /** + * Kiểm tra user có tất cả các role không + */ + hasAllRoles: (roles: UserRole[]) => hasAllRoles(userRoles, roles), + + /** + * Kiểm tra user có quyền truy cập route không + */ + canAccessRoute: (path: string) => canAccessRoute(userRoles, path), + + /** + * Kiểm tra user là admin + */ + isAdmin: () => hasRole(userRoles, "ADMIN"), + + /** + * Kiểm tra user là moderator + */ + isModerator: () => hasRole(userRoles, "MOD"), + + /** + * Kiểm tra user là historian + */ + isHistorian: () => hasRole(userRoles, "HISTORIAN"), + + /** + * Lấy danh sách routes user có thể truy cập + */ + getAvailableRoutes: () => getAvailableRoutes(userRoles), + } +} diff --git a/src/hooks/useRestoreUserData.ts b/src/hooks/useRestoreUserData.ts new file mode 100644 index 0000000..61b98fa --- /dev/null +++ b/src/hooks/useRestoreUserData.ts @@ -0,0 +1,19 @@ +"use client" + +import { useEffect } from "react" +import { useDispatch } from "react-redux" +import { setUserData, clearUserData } from "@/store/features/userSlice" +import { getUserFromCookie } from "@/lib/cookieStorage" + +export const useRestoreUserData = () => { + const dispatch = useDispatch() + + useEffect(() => { + const userData = getUserFromCookie() + if (userData) { + dispatch(setUserData(userData)) + } else { + dispatch(clearUserData()) + } + }, [dispatch]) +} diff --git a/src/layout/AppSidebar.tsx b/src/layout/AppSidebar.tsx index d12a378..51f5fad 100644 --- a/src/layout/AppSidebar.tsx +++ b/src/layout/AppSidebar.tsx @@ -18,6 +18,8 @@ import { UserCircleIcon, } from "../icons/index"; import SidebarWidget from "./SidebarWidget"; +import { useAuth } from "@/hooks/useAuth"; +import { canAccessRoute, PUBLIC_ROUTES } from "@/config/routes.config"; type NavItem = { name: string; @@ -101,6 +103,39 @@ const othersItems: NavItem[] = [ const AppSidebar: React.FC = () => { const { isExpanded, isMobileOpen, isHovered, setIsHovered } = useSidebar(); const pathname = usePathname(); + const { userRoles } = useAuth(); + + const hasAccess = (path?: string) => { + if (!path) return true; + if (!userRoles || userRoles.length === 0) { + return PUBLIC_ROUTES.includes(path) || ["/", "/profile", "/calendar"].includes(path); + } + return canAccessRoute(userRoles, path); + }; + + const buildNavItems = (items: NavItem[]) => + items + .map((nav) => { + if (nav.path && !hasAccess(nav.path)) { + return null; + } + + if (nav.subItems) { + const filteredItems = nav.subItems.filter((subItem) => + hasAccess(subItem.path), + ); + if (filteredItems.length === 0) { + return null; + } + return { ...nav, subItems: filteredItems }; + } + + return nav; + }) + .filter((item): item is NavItem => item !== null); + + const filteredMainNavItems = buildNavItems(navItems); + const filteredOthersNavItems = buildNavItems(othersItems); const renderMenuItems = ( navItems: NavItem[], @@ -357,7 +392,7 @@ const AppSidebar: React.FC = () => { )} - {renderMenuItems(navItems, "main")} + {renderMenuItems(filteredMainNavItems, "main")}
@@ -374,7 +409,7 @@ const AppSidebar: React.FC = () => { )} - {renderMenuItems(othersItems, "others")} + {renderMenuItems(filteredOthersNavItems, "others")}
diff --git a/src/lib/cookieStorage.ts b/src/lib/cookieStorage.ts new file mode 100644 index 0000000..2c9ce3b --- /dev/null +++ b/src/lib/cookieStorage.ts @@ -0,0 +1,37 @@ +import { UserData } from "@/interface/user" + +export const saveUserToCookie = (userData: UserData) => { + if (typeof document === "undefined") return + + const userDataJson = JSON.stringify(userData) + document.cookie = `userDataRedux=${encodeURIComponent(userDataJson)}; path=/; max-age=86400` +} + + +export const removeUserFromCookie = () => { + if (typeof document === "undefined") return + + document.cookie = "userDataRedux=; path=/; max-age=0" +} + +export const getUserFromCookie = (): UserData | null => { + if (typeof document === "undefined") return null + + const name = "userDataRedux=" + const decodedCookie = decodeURIComponent(document.cookie) + const cookieArray = decodedCookie.split(";") + + for (let cookie of cookieArray) { + cookie = cookie.trim() + if (cookie.indexOf(name) === 0) { + try { + const userData = JSON.parse(cookie.substring(name.length)) + return userData + } catch (error) { + console.error("Error parsing user cookie:", error) + return null + } + } + } + return null +} diff --git a/src/store/StoreProvider.tsx b/src/store/StoreProvider.tsx index f4adf37..38495e7 100644 --- a/src/store/StoreProvider.tsx +++ b/src/store/StoreProvider.tsx @@ -3,6 +3,13 @@ import { useRef } from 'react'; import { Provider } from 'react-redux'; import { store } from './store'; +import { useRestoreUserData } from '@/hooks/useRestoreUserData'; + + +function RestoreUserDataComponent() { + useRestoreUserData(); + return null; +} export default function StoreProvider({ children, @@ -10,5 +17,10 @@ export default function StoreProvider({ children: React.ReactNode; }) { const storeRef = useRef(store); - return {children}; + return ( + + + {children} + + ); } \ No newline at end of file diff --git a/src/store/features/userSlice.ts b/src/store/features/userSlice.ts index aed21c5..41711e8 100644 --- a/src/store/features/userSlice.ts +++ b/src/store/features/userSlice.ts @@ -1,5 +1,6 @@ import { UserData } from '@/interface/user'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { getUserFromCookie } from '@/lib/cookieStorage'; const getStoredApplication = () => { if (typeof window !== "undefined") { @@ -9,15 +10,24 @@ const getStoredApplication = () => { return null; }; +const getStoredUserData = (): UserData | null => { + if (typeof window === "undefined") { + return null; + } + return getUserFromCookie(); +}; + interface UserState { data: UserData | null; isAuthenticated: boolean; selectedApplication: any | null; } +const storedUserData = getStoredUserData(); + const initialState: UserState = { - data: null, - isAuthenticated: false, + data: storedUserData, + isAuthenticated: Boolean(storedUserData), selectedApplication: getStoredApplication(), }; @@ -29,6 +39,10 @@ const userSlice = createSlice({ state.data = action.payload; state.isAuthenticated = true; }, + clearUserData: (state) => { + state.data = null; + state.isAuthenticated = false; + }, setSelectedApplication: (state, action: PayloadAction) => { state.selectedApplication = action.payload; if (typeof window !== "undefined") { @@ -44,5 +58,5 @@ const userSlice = createSlice({ }, }); -export const { setUserData, setSelectedApplication, clearSelectedApplication } = userSlice.actions; +export const { setUserData, clearUserData, setSelectedApplication, clearSelectedApplication } = userSlice.actions; export default userSlice.reducer; \ No newline at end of file diff --git a/src/store/store.ts b/src/store/store.ts index c3c8be0..29c2bd1 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -1,4 +1,5 @@ import { configureStore } from '@reduxjs/toolkit'; +import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'; // Thêm dòng này import userReducer from './features/userSlice'; export const store = configureStore({ @@ -8,4 +9,7 @@ export const store = configureStore({ }); export type RootState = ReturnType; -export type AppDispatch = typeof store.dispatch; \ No newline at end of file +export type AppDispatch = typeof store.dispatch; + +export const useAppDispatch = () => useDispatch(); +export const useAppSelector: TypedUseSelectorHook = useSelector; \ No newline at end of file