Commit 330d67d3 authored by Lê Bảo Hồng Đức's avatar Lê Bảo Hồng Đức

fix

parent edbc9f87
...@@ -579,7 +579,7 @@ export default function AdminBaseConfigPage() { ...@@ -579,7 +579,7 @@ export default function AdminBaseConfigPage() {
value="social" 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]" 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> </TabsTrigger>
</TabsList> </TabsList>
...@@ -1022,9 +1022,9 @@ export default function AdminBaseConfigPage() { ...@@ -1022,9 +1022,9 @@ export default function AdminBaseConfigPage() {
<CardHeader> <CardHeader>
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div> <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"> <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> </CardDescription>
</div> </div>
...@@ -1034,7 +1034,7 @@ export default function AdminBaseConfigPage() { ...@@ -1034,7 +1034,7 @@ export default function AdminBaseConfigPage() {
className="rounded-xl bg-[#163b73] text-white hover:bg-[#163b73]/90" className="rounded-xl bg-[#163b73] text-white hover:bg-[#163b73]/90"
> >
<Save className="mr-2 h-4 w-4" /> <Save className="mr-2 h-4 w-4" />
Luu m?ng x? h?i Luu cấu hình
</Button> </Button>
</div> </div>
</CardHeader> </CardHeader>
......
...@@ -159,7 +159,7 @@ export default function AdminDashboardPage() { ...@@ -159,7 +159,7 @@ export default function AdminDashboardPage() {
() => [ () => [
{ {
title: "Cấu hình chung", 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", href: "/admin/base-config",
icon: Globe, icon: Globe,
}, },
...@@ -313,7 +313,7 @@ export default function AdminDashboardPage() { ...@@ -313,7 +313,7 @@ export default function AdminDashboardPage() {
<div className="mt-2 text-2xl font-semibold text-[#163b73]">{activeBanners.length}</div> <div className="mt-2 text-2xl font-semibold text-[#163b73]">{activeBanners.length}</div>
</div> </div>
<div className="rounded-[24px] border border-[#063e8e]/10 bg-white p-4"> <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 className="mt-2 text-2xl font-semibold text-[#163b73]">{visibleSocials.length}</div>
</div> </div>
</div> </div>
......
...@@ -317,6 +317,12 @@ export default function AdminNewsPage() { ...@@ -317,6 +317,12 @@ export default function AdminNewsPage() {
filters.push(`category.id==${categoryFilter}`); filters.push(`category.id==${categoryFilter}`);
} }
if (typeFilter === "tintuc") {
filters.push("type==news");
} else if (typeFilter === "baiviettrang") {
filters.push("type==page");
}
if (statusFilter === "visible") { if (statusFilter === "visible") {
filters.push("is_hidden==false"); filters.push("is_hidden==false");
} else if (statusFilter === "hidden") { } else if (statusFilter === "hidden") {
...@@ -324,7 +330,7 @@ export default function AdminNewsPage() { ...@@ -324,7 +330,7 @@ export default function AdminNewsPage() {
} }
return filters.join(","); return filters.join(",");
}, [categoryFilter, debouncedSearch, statusFilter]); }, [categoryFilter, debouncedSearch, statusFilter, typeFilter]);
const load = React.useCallback(async () => { const load = React.useCallback(async () => {
setReady(false); setReady(false);
...@@ -387,14 +393,6 @@ export default function AdminNewsPage() { ...@@ -387,14 +393,6 @@ export default function AdminNewsPage() {
); );
}, [headerItems]); }, [headerItems]);
const filteredItems = React.useMemo(() => {
if (typeFilter === "all") {
return items;
}
return items.filter((item) => item.type === typeFilter);
}, [items, typeFilter]);
const stats = React.useMemo(() => { const stats = React.useMemo(() => {
return [ return [
{ {
...@@ -452,7 +450,9 @@ export default function AdminNewsPage() { ...@@ -452,7 +450,9 @@ export default function AdminNewsPage() {
actionLabel="Thêm bài viết" actionLabel="Thêm bài viết"
actionIcon={<Plus className="mr-2 h-4 w-4" />} actionIcon={<Plus className="mr-2 h-4 w-4" />}
onSearchChange={setSearch} onSearchChange={setSearch}
onActionClick={() => router.push("/admin/news/new")} onActionClick={() =>
router.push(`/admin/news/new?returnTo=${encodeURIComponent(listPath)}`)
}
filters={ filters={
<div className="flex flex-col gap-3 lg:flex-row lg:items-center"> <div className="flex flex-col gap-3 lg:flex-row lg:items-center">
<Select value={typeFilter} onValueChange={setTypeFilter}> <Select value={typeFilter} onValueChange={setTypeFilter}>
...@@ -531,14 +531,14 @@ export default function AdminNewsPage() { ...@@ -531,14 +531,14 @@ export default function AdminNewsPage() {
<TableBody> <TableBody>
{!ready ? ( {!ready ? (
<AdminNewsTableLoading /> <AdminNewsTableLoading />
) : filteredItems.length === 0 ? ( ) : items.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={7} className="py-12 text-center text-sm text-gray-700"> <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. Không có bài viết nào phù hợp.
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
filteredItems.map((item, index) => { items.map((item, index) => {
const categoryNames = getDisplayCategoryNames(item, headerItems); const categoryNames = getDisplayCategoryNames(item, headerItems);
const primaryCategoryName = categoryNames[0] ?? "\u2014"; const primaryCategoryName = categoryNames[0] ?? "\u2014";
const extraCategoryCount = Math.max(categoryNames.length - 1, 0); const extraCategoryCount = Math.max(categoryNames.length - 1, 0);
......
...@@ -75,43 +75,59 @@ export function AdminAuthGuard({ children }: { children: React.ReactNode }) { ...@@ -75,43 +75,59 @@ export function AdminAuthGuard({ children }: { children: React.ReactNode }) {
const accessTokenExpired = useAuthStore((state) => state.appAccessTokenExpired); const accessTokenExpired = useAuthStore((state) => state.appAccessTokenExpired);
const refreshToken = useAuthStore((state) => state.appRefreshToken); const refreshToken = useAuthStore((state) => state.appRefreshToken);
const isRefreshing = useAuthStore((state) => state.appIsRefreshing); 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(() => { useEffect(() => {
if (!hasHydrated || pathname === LOGIN_PATH) return; if (pathname === LOGIN_PATH) {
setAuthCheckState("ready");
return;
}
if (!hasHydrated) {
setAuthCheckState("idle");
return;
}
let cancelled = false; let cancelled = false;
const restoreSession = async () => { const restoreSession = async () => {
const needsRefresh = Boolean( setAuthCheckState("checking");
const hasValidAccessToken = Boolean(
accessToken && accessToken &&
accessTokenExpired && isLoggedIn &&
accessTokenExpired <= Date.now() && (!accessTokenExpired || accessTokenExpired > Date.now()),
refreshToken,
); );
if (accessToken && isLoggedIn && !needsRefresh) return; if (hasValidAccessToken) {
if (!cancelled) {
setAuthCheckState("ready");
}
return;
}
if (!refreshToken) { if (!refreshToken) {
router.replace(`${LOGIN_PATH}?redirect=${encodeURIComponent(pathname)}`); if (!cancelled) {
setAuthCheckState("ready");
router.replace(`${LOGIN_PATH}?redirect=${redirectParam}`);
}
return; return;
} }
setIsRestoringSession(true);
try { try {
const nextToken = await ensureValidAdminAccessToken(); const nextToken = await ensureValidAdminAccessToken();
if (!nextToken && !cancelled) { if (!nextToken && !cancelled) {
router.replace(`${LOGIN_PATH}?redirect=${encodeURIComponent(pathname)}`); router.replace(`${LOGIN_PATH}?redirect=${redirectParam}`);
} }
} catch { } catch {
if (!cancelled) { if (!cancelled) {
router.replace(`${LOGIN_PATH}?redirect=${encodeURIComponent(pathname)}`); router.replace(`${LOGIN_PATH}?redirect=${redirectParam}`);
} }
} finally { } finally {
if (!cancelled) { if (!cancelled) {
setIsRestoringSession(false); setAuthCheckState("ready");
} }
} }
}; };
...@@ -121,13 +137,22 @@ export function AdminAuthGuard({ children }: { children: React.ReactNode }) { ...@@ -121,13 +137,22 @@ export function AdminAuthGuard({ children }: { children: React.ReactNode }) {
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [accessToken, accessTokenExpired, hasHydrated, isLoggedIn, pathname, refreshToken, router]); }, [
accessToken,
accessTokenExpired,
hasHydrated,
isLoggedIn,
pathname,
redirectParam,
refreshToken,
router,
]);
if (pathname === LOGIN_PATH) { if (pathname === LOGIN_PATH) {
return <>{children}</>; return <>{children}</>;
} }
if (!hasHydrated || isRefreshing || isRestoringSession) { if (!hasHydrated || isRefreshing || authCheckState !== "ready") {
return <AdminAuthLoadingScreen />; return <AdminAuthLoadingScreen />;
} }
......
...@@ -35,33 +35,34 @@ const navigation: NavItem[] = [ ...@@ -35,33 +35,34 @@ const navigation: NavItem[] = [
{ name: 'Quản lý bài viết', href: '/admin/news', icon: Newspaper }, { 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ý tag tìm kiếm', href: '/admin/tags', icon: Tags },
{ name: 'Quản lý video', href: '/admin/videos', icon: Video }, { name: 'Quản lý video', href: '/admin/videos', icon: Video },
{ // {
name: 'Quản lý hội viên', // name: 'Quản lý hội viên',
icon: Users, // icon: Users,
children: [ // children: [
{ name: 'Danh sách hội viên', href: '/admin/members' }, // { 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ý lĩnh vực', href: '/admin/members/fields' },
{ name: 'Quản lý khu vực', href: '/admin/members/regions' }, // { name: 'Quản lý khu vực', href: '/admin/members/regions' },
], // ],
}, // },
{ // {
name: 'Quản lý liên hệ', // name: 'Quản lý liên hệ',
icon: Mail, // icon: Mail,
children: [ // children: [
{ // {
name: 'Quản lý Email đăng ký nhận thông tin', // name: 'Quản lý Email đăng ký nhận thông tin',
href: '/admin/contact-management/newsletter-emails', // href: '/admin/contact-management/newsletter-emails',
}, // },
{ // {
name: 'Quản lý Đơn liên hệ', // name: 'Quản lý Đơn liên hệ',
href: '/admin/contact-management/contact-requests', // href: '/admin/contact-management/contact-requests',
}, // },
{ // {
name: 'Quản lý Đơn đăng ký hội viên', // name: 'Quản lý Đơn đăng ký hội viên',
href: '/admin/contact-management/membership-applications', // 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 }, { name: 'Quản lý ảnh', href: '/admin/media', icon: ImagePlus },
]; ];
......
...@@ -7,7 +7,7 @@ import useAuthStore, { ...@@ -7,7 +7,7 @@ import useAuthStore, {
} from "@/store/useAuthStore"; } from "@/store/useAuthStore";
const AUTH_BASE_URL = `${process.env.NEXT_PUBLIC_BACKEND_HOST}/api/v1.0/auth`; 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> { interface AuthEnvelope<T> {
message?: string | null; message?: string | null;
...@@ -50,6 +50,7 @@ interface RefreshResponseData { ...@@ -50,6 +50,7 @@ interface RefreshResponseData {
interface AuthRequestOptions extends RequestInit { interface AuthRequestOptions extends RequestInit {
skipAuthHeader?: boolean; skipAuthHeader?: boolean;
authToken?: string | null;
} }
type AuthFailureReason = "missing_refresh_token" | "refresh_failed"; type AuthFailureReason = "missing_refresh_token" | "refresh_failed";
...@@ -114,7 +115,7 @@ async function requestAuth<T>( ...@@ -114,7 +115,7 @@ async function requestAuth<T>(
headers.set("Content-Type", "application/json"); headers.set("Content-Type", "application/json");
if (!init?.skipAuthHeader) { if (!init?.skipAuthHeader) {
const token = useAuthStore.getState().appAccessToken; const token = init?.authToken ?? useAuthStore.getState().appAccessToken;
if (token && !headers.has("Authorization")) { if (token && !headers.has("Authorization")) {
headers.set("Authorization", `Bearer ${token}`); headers.set("Authorization", `Bearer ${token}`);
} }
...@@ -178,6 +179,7 @@ export async function loginAdmin(email: string, password: string) { ...@@ -178,6 +179,7 @@ export async function loginAdmin(email: string, password: string) {
const me = await requestAuth<MeResponseData>("/me", { const me = await requestAuth<MeResponseData>("/me", {
method: "GET", method: "GET",
authToken: payload.access_token,
}).catch(() => payload.user ?? null); }).catch(() => payload.user ?? null);
const normalizedUser = normalizeUser(me ?? payload.user); const normalizedUser = normalizeUser(me ?? payload.user);
......
...@@ -49,7 +49,7 @@ export interface AuthStoreStateType { ...@@ -49,7 +49,7 @@ export interface AuthStoreStateType {
remember: boolean; remember: boolean;
} | null; } | null;
_hasHydrated: boolean; _hasHydrated: boolean;
setHasHydrated: (state: AuthStoreStateType) => void; setHasHydrated: (hasHydrated?: boolean) => void;
setAppIsLoggedIn: (isLoggedIn: boolean) => void; setAppIsLoggedIn: (isLoggedIn: boolean) => void;
setAuthSession: (payload: AuthSessionPayload) => void; setAuthSession: (payload: AuthSessionPayload) => void;
updateAccessToken: (payload: AuthRefreshPayload) => void; updateAccessToken: (payload: AuthRefreshPayload) => void;
...@@ -94,9 +94,9 @@ const useAuthStore = create<AuthStoreStateType>()( ...@@ -94,9 +94,9 @@ const useAuthStore = create<AuthStoreStateType>()(
persist( persist(
(set, get) => ({ (set, get) => ({
...baseState, ...baseState,
setHasHydrated: (state: AuthStoreStateType) => setHasHydrated: (hasHydrated = true) =>
set(() => ({ set(() => ({
_hasHydrated: state != undefined, _hasHydrated: hasHydrated,
})), })),
setAppIsLoggedIn: (isLoggedIn: boolean) => setAppIsLoggedIn: (isLoggedIn: boolean) =>
set(() => ({ set(() => ({
...@@ -184,10 +184,13 @@ const useAuthStore = create<AuthStoreStateType>()( ...@@ -184,10 +184,13 @@ const useAuthStore = create<AuthStoreStateType>()(
appSessionExpiredNotified: state.appSessionExpiredNotified, appSessionExpiredNotified: state.appSessionExpiredNotified,
appUserRemember: state.appUserRemember, appUserRemember: state.appUserRemember,
}), }),
onRehydrateStorage: () => { onRehydrateStorage: (state) => {
return (state: AuthStoreStateType | undefined, error: unknown) => { return (state: AuthStoreStateType | undefined, error: unknown) => {
if (error || state == undefined) return; if (error) {
state.setHasHydrated(state); useAuthStore.persist.clearStorage();
}
(state ?? useAuthStore.getState()).setHasHydrated(true);
}; };
}, },
}, },
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment