feat: implement authentication modules and add daily statistics dashboard component
Build and Release / release (push) Successful in 28s
Build and Release / release (push) Successful in 28s
This commit is contained in:
@@ -1,13 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useSidebar } from "@/context/SidebarContext";
|
import { useSidebar } from "@/context/SidebarContext";
|
||||||
|
import { UserData } from "@/interface/user";
|
||||||
import AppHeader from "@/layout/AppHeader";
|
import AppHeader from "@/layout/AppHeader";
|
||||||
import AppSidebar from "@/layout/AppSidebar";
|
import AppSidebar from "@/layout/AppSidebar";
|
||||||
import Backdrop from "@/layout/Backdrop";
|
import Backdrop from "@/layout/Backdrop";
|
||||||
import { apiGetCurrentUser } from "@/service/auth";
|
import { apiGetCurrentUser } from "@/service/auth";
|
||||||
import { setUserData } from "@/store/features/userSlice";
|
import { setUserData } from "@/store/features/userSlice";
|
||||||
import React, { useEffect } from "react";
|
import { RootState } from "@/store/store";
|
||||||
import { useDispatch } from "react-redux";
|
import { useRouter } from "next/navigation";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export default function AdminLayout({
|
export default function AdminLayout({
|
||||||
children,
|
children,
|
||||||
@@ -15,20 +19,44 @@ export default function AdminLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { isExpanded, isHovered, isMobileOpen } = useSidebar();
|
const { isExpanded, isHovered, isMobileOpen } = useSidebar();
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch();
|
||||||
|
const router = useRouter();
|
||||||
|
const userData = useSelector((state: RootState) => state.user.data);
|
||||||
|
const [isLoading, setIsLoading] = useState(!userData);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (userData) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const fetchUser = async () => {
|
const fetchUser = async () => {
|
||||||
try {
|
try {
|
||||||
const userData = await apiGetCurrentUser();
|
setIsLoading(true);
|
||||||
dispatch(setUserData(userData.data));
|
const res = await apiGetCurrentUser();
|
||||||
} catch (err) {
|
const userData: UserData = res;
|
||||||
console.error("Lỗi:", err);
|
|
||||||
|
const allowedRoles = ["ADMIN", "MOD", "HISTORIAN"];
|
||||||
|
|
||||||
|
const isBanned = userData.roles.some((role) => role.name === "BANNED");
|
||||||
|
|
||||||
|
const hasPermission = userData.roles.some((role) =>
|
||||||
|
allowedRoles.includes(role.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isBanned || !hasPermission) {
|
||||||
|
toast.error("Bạn không có quyền truy cập");
|
||||||
|
router.replace("/auth/signin");
|
||||||
|
}
|
||||||
|
dispatch(setUserData(res.data));
|
||||||
|
} catch {
|
||||||
|
router.replace("/auth/signin");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchUser();
|
fetchUser();
|
||||||
}, [])
|
}, [dispatch, userData, router]);
|
||||||
|
|
||||||
|
|
||||||
// Dynamic class for main content margin based on sidebar state
|
// Dynamic class for main content margin based on sidebar state
|
||||||
const mainContentMargin = isMobileOpen
|
const mainContentMargin = isMobileOpen
|
||||||
@@ -37,6 +65,29 @@ export default function AdminLayout({
|
|||||||
? "lg:ml-[290px]"
|
? "lg:ml-[290px]"
|
||||||
: "lg:ml-[90px]";
|
: "lg:ml-[90px]";
|
||||||
|
|
||||||
|
if (isLoading || !userData) {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white dark:bg-gray-900">
|
||||||
|
<div className="flex flex-col items-center gap-5">
|
||||||
|
{/* Spinner */}
|
||||||
|
<div className="relative h-14 w-14">
|
||||||
|
<div className="absolute inset-0 rounded-full border-4 border-gray-200 dark:border-gray-700" />
|
||||||
|
<div className="absolute inset-0 rounded-full border-4 border-transparent border-t-blue-500 animate-spin" />
|
||||||
|
</div>
|
||||||
|
{/* Text */}
|
||||||
|
<div className="flex flex-col items-center gap-1.5">
|
||||||
|
<p className="text-base font-semibold text-gray-800 dark:text-gray-100">
|
||||||
|
Đang tải dữ liệu
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 animate-pulse">
|
||||||
|
Vui lòng chờ trong giây lát...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen xl:flex">
|
<div className="min-h-screen xl:flex">
|
||||||
{/* Sidebar and Backdrop */}
|
{/* Sidebar and Backdrop */}
|
||||||
|
|||||||
-4
@@ -1,9 +1,5 @@
|
|||||||
import GridShape from "@/components/common/GridShape";
|
|
||||||
import ThemeTogglerTwo from "@/components/common/ThemeTogglerTwo";
|
import ThemeTogglerTwo from "@/components/common/ThemeTogglerTwo";
|
||||||
|
|
||||||
import { ThemeProvider } from "@/context/ThemeContext";
|
import { ThemeProvider } from "@/context/ThemeContext";
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default function AuthLayout({
|
export default function AuthLayout({
|
||||||
+1
-1
@@ -100,7 +100,7 @@ export default function ResetPasswordForm() {
|
|||||||
<div className="flex flex-col flex-1 w-full lg:w-1/2 overflow-y-auto no-scrollbar">
|
<div className="flex flex-col flex-1 w-full lg:w-1/2 overflow-y-auto no-scrollbar">
|
||||||
<div className="w-full max-w-md mx-auto mb-5 sm:pt-10">
|
<div className="w-full max-w-md mx-auto mb-5 sm:pt-10">
|
||||||
<Link
|
<Link
|
||||||
href="/signin"
|
href="/auth/signin"
|
||||||
className="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
className="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon />
|
<ChevronLeftIcon />
|
||||||
@@ -71,7 +71,6 @@ export default function SignInForm() {
|
|||||||
toast.success("Đăng nhập thành công!");
|
toast.success("Đăng nhập thành công!");
|
||||||
const data = await apiGetCurrentUser();
|
const data = await apiGetCurrentUser();
|
||||||
|
|
||||||
// console.log("Current User Data:", data);
|
|
||||||
if (data?.data) {
|
if (data?.data) {
|
||||||
dispatch(setUserData(data.data));
|
dispatch(setUserData(data.data));
|
||||||
router.push("/");
|
router.push("/");
|
||||||
@@ -79,7 +78,7 @@ export default function SignInForm() {
|
|||||||
} else {
|
} else {
|
||||||
toast.error("Email hoặc mật khẩu không đúng.");
|
toast.error("Email hoặc mật khẩu không đúng.");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
setErrorMsg("Lỗi khi đăng nhập. Vui lòng thử lại.");
|
setErrorMsg("Lỗi khi đăng nhập. Vui lòng thử lại.");
|
||||||
toast.error("Đăng nhập thất bại. Vui lòng kiểm tra lại thông tin.");
|
toast.error("Đăng nhập thất bại. Vui lòng kiểm tra lại thông tin.");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -206,7 +205,7 @@ export default function SignInForm() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/reset-password"
|
href="/auth/reset-password"
|
||||||
className="text-sm text-brand-500 hover:text-brand-600 dark:text-brand-400"
|
className="text-sm text-brand-500 hover:text-brand-600 dark:text-brand-400"
|
||||||
>
|
>
|
||||||
Forgot password?
|
Forgot password?
|
||||||
@@ -256,7 +255,7 @@ export default function SignInForm() {
|
|||||||
<p className="text-sm font-normal text-center text-gray-700 dark:text-gray-400 sm:text-start">
|
<p className="text-sm font-normal text-center text-gray-700 dark:text-gray-400 sm:text-start">
|
||||||
Don't have an account? {""}
|
Don't have an account? {""}
|
||||||
<Link
|
<Link
|
||||||
href="/signup"
|
href="/auth/signup"
|
||||||
className="text-brand-500 hover:text-brand-600 dark:text-brand-400"
|
className="text-brand-500 hover:text-brand-600 dark:text-brand-400"
|
||||||
>
|
>
|
||||||
Sign Up
|
Sign Up
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default function SignUpForm() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
await apiCreateOTP(formData.email);
|
await apiCreateOTP(formData.email);
|
||||||
setStep(2);
|
setStep(2);
|
||||||
} catch (error) {
|
} catch {
|
||||||
setErrorMsg("Lỗi khi tạo OTP. Vui lòng thử lại.");
|
setErrorMsg("Lỗi khi tạo OTP. Vui lòng thử lại.");
|
||||||
toast.error("Tạo OTP thất bại. Vui lòng kiểm tra lại thông tin.");
|
toast.error("Tạo OTP thất bại. Vui lòng kiểm tra lại thông tin.");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -106,7 +106,7 @@ export default function SignUpForm() {
|
|||||||
token_id: tokenId,
|
token_id: tokenId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const signupRes = await apiSignUp(signupPayload);
|
await apiSignUp(signupPayload);
|
||||||
|
|
||||||
await Swal.fire({
|
await Swal.fire({
|
||||||
title: "Tạo tài khoản thành công!. Quay về trang đăng nhập để tiếp tục",
|
title: "Tạo tài khoản thành công!. Quay về trang đăng nhập để tiếp tục",
|
||||||
@@ -131,7 +131,7 @@ export default function SignUpForm() {
|
|||||||
<div className="flex flex-col flex-1 lg:w-1/2 w-full overflow-y-auto no-scrollbar">
|
<div className="flex flex-col flex-1 lg:w-1/2 w-full overflow-y-auto no-scrollbar">
|
||||||
<div className="w-full max-w-md sm:pt-10 mx-auto mb-5">
|
<div className="w-full max-w-md sm:pt-10 mx-auto mb-5">
|
||||||
<Link
|
<Link
|
||||||
href="/signin"
|
href="/auth/signin"
|
||||||
className="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
className="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon />
|
<ChevronLeftIcon />
|
||||||
@@ -332,7 +332,7 @@ export default function SignUpForm() {
|
|||||||
<p className="text-sm font-normal text-center text-gray-700 dark:text-gray-400 sm:text-start">
|
<p className="text-sm font-normal text-center text-gray-700 dark:text-gray-400 sm:text-start">
|
||||||
Already have an account?{" "}
|
Already have an account?{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/signin"
|
href="/auth/signin"
|
||||||
className="text-brand-500 hover:text-brand-600 dark:text-brand-400"
|
className="text-brand-500 hover:text-brand-600 dark:text-brand-400"
|
||||||
>
|
>
|
||||||
Sign In
|
Sign In
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ export default function MonthlyNewChart() {
|
|||||||
try {
|
try {
|
||||||
const response = await getStatistics({ start_date: start, end_date: end });
|
const response = await getStatistics({ start_date: start, end_date: end });
|
||||||
if (response?.data) {
|
if (response?.data) {
|
||||||
// Tổng hợp theo tháng
|
|
||||||
const monthsMap: { [key: string]: number } = {};
|
const monthsMap: { [key: string]: number } = {};
|
||||||
|
|
||||||
response.data.forEach(item => {
|
response.data.forEach(item => {
|
||||||
@@ -56,8 +55,7 @@ export default function MonthlyNewChart() {
|
|||||||
|
|
||||||
setChartData(aggregated);
|
setChartData(aggregated);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error("Failed to fetch chart data", error);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ api.interceptors.response.use(
|
|||||||
isRefreshing = true;
|
isRefreshing = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.post(`${baseURL}/auth/refresh`, {}, { withCredentials: true });
|
await axios.post(`${baseURL}/auth/refresh`, undefined, { withCredentials: true });
|
||||||
|
|
||||||
processQueue(null);
|
processQueue(null);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user