diff --git a/src/components/academy/CommentsModal.tsx b/src/components/academy/CommentsModal.tsx index d9436f3..e7b2f03 100644 --- a/src/components/academy/CommentsModal.tsx +++ b/src/components/academy/CommentsModal.tsx @@ -10,6 +10,7 @@ import Rate from "rc-rate"; import "rc-rate/assets/index.css"; import { useUser } from "@/hooks/useUser"; import { Course } from "@/types/types"; +import { useRequireAuth } from "@/lib/auth/useRequireAuth"; export interface Comments { _id: string; @@ -60,6 +61,7 @@ function CommentsModal({ const { request } = useAxios(); const containerRef = useRef(null); const currentUser = useUser(); + const requireAuth = useRequireAuth(); const [hasRatedCourse, setHasRatedCourse] = useState(false); // تابع دریافت اطلاعات کاربر با caching @@ -215,6 +217,7 @@ function CommentsModal({ // ارسال کامنت جدید const sendMessageHandler = async () => { + if (!requireAuth()) return; if (!newMessage.trim()) { toast.error("ثبت نظر الزامی است."); return; diff --git a/src/components/academy/MainModelCardActions.tsx b/src/components/academy/MainModelCardActions.tsx index 207f0e2..d295ddc 100644 --- a/src/components/academy/MainModelCardActions.tsx +++ b/src/components/academy/MainModelCardActions.tsx @@ -14,6 +14,7 @@ import { useUserById } from "@/hooks/getUserById"; import { AxiosError } from "axios"; import useInfiniteScroll from "@/hooks/useInfiniteScroll"; import { useRouter } from "next/navigation"; +import { useRequireAuth } from "@/lib/auth/useRequireAuth"; function MainModelCardActions({ postData }: { postData: Course }) { const { _id, user_id, likes } = postData; @@ -35,6 +36,7 @@ function MainModelCardActions({ postData }: { postData: Course }) { const { request, loading } = useAxios(); const router = useRouter(); + const requireAuth = useRequireAuth(); useEffect(() => { const fetchData = async () => { try { @@ -56,6 +58,7 @@ function MainModelCardActions({ postData }: { postData: Course }) { }, []); const toggleLike = async () => { + if (!requireAuth()) return; if (loading) return; if (liked === false) { diff --git a/src/components/billboards/MainBillboardCard/BillboardCommentsModal.tsx b/src/components/billboards/MainBillboardCard/BillboardCommentsModal.tsx index db37c3e..9b24f02 100644 --- a/src/components/billboards/MainBillboardCard/BillboardCommentsModal.tsx +++ b/src/components/billboards/MainBillboardCard/BillboardCommentsModal.tsx @@ -6,6 +6,7 @@ import VerificationBadge from "@/components/main/VerificationBadge"; import ProfileAvatar from "@/components/main/ProfileAvatar"; import RoundedInput from "@/components/elements/RoundedInput"; import useAxios from "@/hooks/useAxios"; +import { useRequireAuth } from "@/lib/auth/useRequireAuth"; export interface Comments { _id: string; @@ -46,7 +47,9 @@ function BillboardCommentsModal({ }); const [newMessage, setNewMessage] = useState(""); const { request } = useAxios(); + const requireAuth = useRequireAuth(); const sendMessageHandler = async () => { + if (!requireAuth()) return; try { const data = { text: newMessage, diff --git a/src/components/models/MainModelCard/CommentsModal.tsx b/src/components/models/MainModelCard/CommentsModal.tsx index 8449e10..09b71ac 100644 --- a/src/components/models/MainModelCard/CommentsModal.tsx +++ b/src/components/models/MainModelCard/CommentsModal.tsx @@ -8,6 +8,7 @@ import Rate from "rc-rate"; import "rc-rate/assets/index.css"; import BoldIcon from "@/components/ui/BoldIcon"; import ProfileAvatar from "@/components/main/ProfileAvatar"; +import { useRequireAuth } from "@/lib/auth/useRequireAuth"; export interface Comments { _id: string; @@ -40,6 +41,7 @@ function CommentsModal({ isOpen, onClose, userId, postId }: CommentsModalProps) const [newMessage, setNewMessage] = useState(""); const [rating, setRating] = useState(0); const { request } = useAxios(); + const requireAuth = useRequireAuth(); const { data, containerRef, isFetchingNextPage, refetch } = useInfiniteScroll({ endpoint: "/users/comments", @@ -66,6 +68,7 @@ function CommentsModal({ isOpen, onClose, userId, postId }: CommentsModalProps) : allComments; const sendMessageHandler = async () => { + if (!requireAuth()) return; if (!newMessage.trim()) { toast.error("ثبت نظر الزامی است."); return; diff --git a/src/components/models/MainModelCard/MainModelCardActions.tsx b/src/components/models/MainModelCard/MainModelCardActions.tsx index 705ff60..0dbd03d 100644 --- a/src/components/models/MainModelCard/MainModelCardActions.tsx +++ b/src/components/models/MainModelCard/MainModelCardActions.tsx @@ -14,6 +14,7 @@ import { useUserById } from "@/hooks/getUserById"; import { AxiosError } from "axios"; import useInfiniteScroll from "@/hooks/useInfiniteScroll"; import SharePostModal from "@/components/main/SharePostModal"; +import { useRequireAuth } from "@/lib/auth/useRequireAuth"; function MainModelCardActions({ postData }: { postData: Post }) { const { @@ -41,8 +42,10 @@ function MainModelCardActions({ postData }: { postData: Post }) { const [likesCountState, setLikesCount] = useState(likesCount || 0); const { request, loading } = useAxios(); + const requireAuth = useRequireAuth(); const toggleLike = async () => { + if (!requireAuth()) return; if (!user_id || !postId) { toast.error("لطفاً وارد حساب کاربری خود شوید یا ثبت‌نام کنید"); return; diff --git a/src/components/models/ModelPage/ModelContent.tsx b/src/components/models/ModelPage/ModelContent.tsx index 1edf22e..709d67d 100644 --- a/src/components/models/ModelPage/ModelContent.tsx +++ b/src/components/models/ModelPage/ModelContent.tsx @@ -14,6 +14,7 @@ import Cookies from "js-cookie"; import { useRouter } from "next/navigation"; import toast from "react-hot-toast"; import { debounce } from "lodash"; +import { useRequireAuth } from "@/lib/auth/useRequireAuth"; const LocationModal = dynamic(() => import("./LocationModal"), { @@ -52,7 +53,9 @@ function ModelContent({ user, searchParams }: ModelContentProps) { }, []); const router = useRouter(); + const requireAuth = useRequireAuth(); const sendMsgHandler = () => { + if (!requireAuth()) return; if (user?.offer_status) { if (usertype) { router.push(`/settings/chats/${user?.user_name}/${user?._id}`); @@ -87,6 +90,11 @@ function ModelContent({ user, searchParams }: ModelContentProps) { fetchData(); }, [searchParams]); + const openCollaboration = () => { + if (!requireAuth()) return; + setShowModelDetailModal((prev) => !prev); + }; + // Debounce برای جلوگیری از باز شدن سریع مودال const openLocationModal = debounce(() => { if (user?.show_location) { @@ -189,7 +197,7 @@ function ModelContent({ user, searchParams }: ModelContentProps) { ) : user?.user_type == "user" ? (
setShowModelDetailModal(!showModelDetailModal)} + onClick={openCollaboration} className="text-xs h-7 md:h-8 max-w-full max-sm:text-[8px]" > درخواست همکاری @@ -211,7 +219,7 @@ function ModelContent({ user, searchParams }: ModelContentProps) { ) : (
setShowModelDetailModal(!showModelDetailModal)} + onClick={openCollaboration} className="text-xs h-7 md:h-8 max-w-full max-sm:text-[8px]" > دعوت به همکاری diff --git a/src/components/posts/ReelsPostActions.tsx b/src/components/posts/ReelsPostActions.tsx index 6f8a3b6..d4f32e1 100644 --- a/src/components/posts/ReelsPostActions.tsx +++ b/src/components/posts/ReelsPostActions.tsx @@ -17,6 +17,7 @@ import SharePostModal from "@/components/main/SharePostModal"; import BoldIcon from "@/components/ui/BoldIcon"; import Cookies from "js-cookie"; import { trackExploreInteraction } from "@/api/trackExploreInteraction"; +import { useRequireAuth } from "@/lib/auth/useRequireAuth"; interface ReelsPostActionsProps { postData: Post; @@ -50,8 +51,10 @@ export default function ReelsPostActions({ postData }: ReelsPostActionsProps) { const [likesCountState, setLikesCount] = useState(likesCount || 0); const { request, loading } = useAxios(); + const requireAuth = useRequireAuth(); const toggleLike = async () => { + if (!requireAuth()) return; if (!user_id || !postId) { toast.error("لطفاً وارد حساب کاربری خود شوید یا ثبت‌نام کنید"); return; diff --git a/src/hooks/useAxios.tsx b/src/hooks/useAxios.tsx index cdcc504..997bdfc 100644 --- a/src/hooks/useAxios.tsx +++ b/src/hooks/useAxios.tsx @@ -117,6 +117,11 @@ axiosInstance.interceptors.response.use( const handleUnauthorized = (): void => { if (typeof window === "undefined") return; + // مهمان (بدون توکن) نباید به صفحه ورود هدایت شود؛ فقط آزادانه صفحات را می‌بیند + // و درخواست‌های محافظت‌شده بی‌سروصدا رد می‌شوند. عملیات‌ها خودشان کاربر را به ورود می‌برند. + const hasToken = Boolean(Cookies.get("token")); + if (!hasToken) return; + const path = window.location.pathname; if ( path.startsWith("/login") || diff --git a/src/hooks/useUser.tsx b/src/hooks/useUser.tsx index 9dbb9ae..854229d 100644 --- a/src/hooks/useUser.tsx +++ b/src/hooks/useUser.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from "react"; import { User } from "@/types/types"; import useAxios from "./useAxios"; -import { getStoredUserId } from "@/lib/auth/session"; +import { getStoredUserId, isAuthenticated } from "@/lib/auth/session"; function readStoredUser(): User | undefined { const id = getStoredUserId(); @@ -23,6 +23,8 @@ export const useUser = () => { } }; useEffect(() => { + // مهمان پروفایل شخصی ندارد؛ از فچ بی‌مورد /profile جلوگیری می‌شود + if (!isAuthenticated()) return; fetchUser(); }, []); diff --git a/src/lib/auth/useRequireAuth.ts b/src/lib/auth/useRequireAuth.ts new file mode 100644 index 0000000..af18189 --- /dev/null +++ b/src/lib/auth/useRequireAuth.ts @@ -0,0 +1,34 @@ +"use client"; + +import { useCallback } from "react"; +import { useRouter } from "next/navigation"; +import toast from "react-hot-toast"; +import { isAuthenticated } from "@/lib/auth/session"; + +/** + * گارد احراز هویت برای عملیات‌ها (لایک، کامنت، فالو، ارسال پیام، درخواست همکاری و ...). + * + * کاربر مهمان می‌تواند آزادانه صفحات را ببیند؛ فقط زمانی که بخواهد عملیاتی انجام دهد، + * پیام کوتاهی نمایش داده و به صفحه ورود با آدرس بازگشت (redirect) هدایت می‌شود. + * + * خروجی `true` یعنی کاربر وارد شده و می‌توان عملیات را ادامه داد. + */ +export function useRequireAuth() { + const router = useRouter(); + + return useCallback( + (message: string | null = "برای انجام این کار وارد حساب کاربری خود شوید"): boolean => { + if (isAuthenticated()) return true; + + if (message) toast.error(message); + + const current = + typeof window !== "undefined" + ? window.location.pathname + window.location.search + : "/"; + router.push(`/login?redirect=${encodeURIComponent(current)}`); + return false; + }, + [router] + ); +} diff --git a/src/middleware.ts b/src/middleware.ts index 72ac8bc..24b9bee 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -12,7 +12,14 @@ export function middleware(req: NextRequest) { const token = req.cookies.get("token")?.value?.trim(); - if (req.nextUrl.pathname.startsWith("/settings") && !token) { + // صفحاتی که ذاتاً نیازمند ورود هستند (حساب کاربری و ساخت محتوا) + // بقیه صفحات برای مهمان آزاد است و فقط هنگام انجام عملیات به ورود هدایت می‌شود. + const protectedPaths = ["/settings", "/new-post", "/new-project"]; + const needsAuth = protectedPaths.some((p) => + req.nextUrl.pathname.startsWith(p) + ); + + if (needsAuth && !token) { const loginUrl = new URL("/login", req.url); loginUrl.searchParams.set("redirect", req.nextUrl.pathname); return NextResponse.redirect(loginUrl); @@ -22,6 +29,6 @@ export function middleware(req: NextRequest) { } // export const config = { - matcher: ["/settings/:path*"], + matcher: ["/settings/:path*", "/new-post/:path*", "/new-project/:path*"], }; \ No newline at end of file