From cbdc7edbf50ebf764939a0ad3ec963f1994f3c2e Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sat, 23 May 2026 16:40:03 +0330 Subject: [PATCH] auth provider + private route --- src/App.tsx | 69 ++++--------------------- src/config/Paths.ts | 11 ++++ src/context/AuthContext.tsx | 73 +++++++++++++++++++++++++++ src/pages/home/service/HomeService.ts | 10 ++-- src/router/MainRouter.tsx | 46 +++++++++++++++-- src/router/PrivateRoute.tsx | 41 +++++++++++++++ 6 files changed, 181 insertions(+), 69 deletions(-) create mode 100644 src/context/AuthContext.tsx create mode 100644 src/router/PrivateRoute.tsx diff --git a/src/App.tsx b/src/App.tsx index 6ad13ad..9077870 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,24 +1,15 @@ import FaJson from "@/langs/fa.json"; +import { AuthProvider } from "@/context/AuthContext"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import i18next from "i18next"; -import { useEffect, useState, type FC } from "react"; +import { type FC } from "react"; import { I18nextProvider } from "react-i18next"; import "react-multi-date-picker/styles/layouts/mobile.css"; import { BrowserRouter } from "react-router-dom"; import "./assets/fonts/irancell/style.css"; import ToastContainer from "./components/Toast"; -import { getToken, setRefreshToken, setToken } from "./config/func"; -import { Paths } from "./config/Paths"; import MainRouter from "./router/MainRouter"; -const isViewerPathname = (pathname: string) => { - const viewerPathPrefix = `${Paths.viewer}/`; - return ( - pathname.startsWith(viewerPathPrefix) && - pathname.length > viewerPathPrefix.length - ); -}; - i18next.init({ interpolation: { escapeValue: false }, lng: "fa", @@ -32,56 +23,16 @@ i18next.init({ const queryClient = new QueryClient(); const App: FC = () => { - const [isLogin, setIsLogin] = useState<"checking" | "isLogin" | "isNotLogin">( - "checking", - ); - - useEffect(() => { - // استخراج توکن‌ها از URL در صورت وجود - const urlParams = new URLSearchParams(window.location.search); - const tokenFromUrl = urlParams.get("token"); - const refreshTokenFromUrl = urlParams.get("refreshToken"); - - if (tokenFromUrl && refreshTokenFromUrl) { - // ذخیره توکن‌ها (جایگزین کردن توکن‌های قبلی اگر وجود داشته باشند) - setToken(tokenFromUrl); - setRefreshToken(refreshTokenFromUrl); - - // حذف پارامترها از URL - const newUrl = window.location.pathname; - window.history.replaceState({}, "", newUrl); - - // تنظیم وضعیت لاگین و هدایت به صفحه اصلی - setIsLogin("isLogin"); - // if (window.location.pathname === Paths.auth.login) { - // window.location.href = Paths.home - // } - return; - } - - const token = getToken(); - if (token) { - setIsLogin("isLogin"); - } else if (isViewerPathname(window.location.pathname)) { - setIsLogin("isLogin"); - } else { - setIsLogin("isNotLogin"); - if (window.location.href.split("auth").length === 1) { - // window.location.href = Paths.auth.login - } - } - }, []); - - console.log("isLogin", isLogin); - return ( - - - - - - + + + + + + + + ); }; diff --git a/src/config/Paths.ts b/src/config/Paths.ts index 0506b56..92e8207 100644 --- a/src/config/Paths.ts +++ b/src/config/Paths.ts @@ -9,3 +9,14 @@ export const Paths = { request: "/designer/request", }, }; + +const viewerPathPrefix = `${Paths.viewer}/`; + +/** Routes that do not require login (must stay in sync with PrivateRoute requireAuth={false}) */ +export const isPublicPath = (pathname: string): boolean => { + return ( + pathname === Paths.home || + (pathname.startsWith(viewerPathPrefix) && + pathname.length > viewerPathPrefix.length) + ); +}; diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx new file mode 100644 index 0000000..aa8db8f --- /dev/null +++ b/src/context/AuthContext.tsx @@ -0,0 +1,73 @@ +import { getToken, setRefreshToken, setToken } from '@/config/func' +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + type FC, + type ReactNode, +} from 'react' + +export type AuthStatus = 'checking' | 'authenticated' | 'unauthenticated' + +type AuthContextValue = { + status: AuthStatus + isAuthenticated: boolean + isChecking: boolean + refreshAuth: () => void +} + +const AuthContext = createContext(null) + +type AuthProviderProps = { + children: ReactNode +} + +export const AuthProvider: FC = ({ children }) => { + const [status, setStatus] = useState('checking') + + const refreshAuth = useCallback(() => { + setStatus(getToken() ? 'authenticated' : 'unauthenticated') + }, []) + + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search) + const tokenFromUrl = urlParams.get('token') + const refreshTokenFromUrl = urlParams.get('refreshToken') + + if (tokenFromUrl && refreshTokenFromUrl) { + setToken(tokenFromUrl) + setRefreshToken(refreshTokenFromUrl) + + const newUrl = window.location.pathname + window.history.replaceState({}, '', newUrl) + + setStatus('authenticated') + return + } + + refreshAuth() + }, [refreshAuth]) + + const value = useMemo( + () => ({ + status, + isAuthenticated: status === 'authenticated', + isChecking: status === 'checking', + refreshAuth, + }), + [status, refreshAuth], + ) + + return {children} +} + +export const useAuth = (): AuthContextValue => { + const context = useContext(AuthContext) + if (!context) { + throw new Error('useAuth must be used within AuthProvider') + } + return context +} diff --git a/src/pages/home/service/HomeService.ts b/src/pages/home/service/HomeService.ts index 19f1c61..8a61137 100644 --- a/src/pages/home/service/HomeService.ts +++ b/src/pages/home/service/HomeService.ts @@ -1,12 +1,12 @@ import axios from "@/config/axios"; -import type { - CreateCatalogParamsType, - UpdateCatalogParamsType, -} from "../types/Types"; import type { CatalogByIdResponseType, CatalogResponseType, } from "@/pages/catalogue/types/Types"; +import type { + CreateCatalogParamsType, + UpdateCatalogParamsType, +} from "../types/Types"; export const createCatalog = async (params: CreateCatalogParamsType) => { const { data } = await axios.post("/admin/catalogue", params); @@ -20,7 +20,7 @@ export const getCatalogs = async () => { export const getCatalogById = async (id: string) => { const { data } = await axios.get( - `/admin/catalogue/${id}`, + `/public/catalogue/${id}`, ); return data; }; diff --git a/src/router/MainRouter.tsx b/src/router/MainRouter.tsx index 118fca6..14e8d4b 100644 --- a/src/router/MainRouter.tsx +++ b/src/router/MainRouter.tsx @@ -2,6 +2,7 @@ import SideBar from '@/shared/SideBar' import Header from '@/shared/Header' import { Route, Routes, useLocation } from 'react-router-dom' import { Paths } from '@/config/Paths' +import PrivateRoute from '@/router/PrivateRoute' import { clx } from '@/helpers/utils' import { useSharedStore } from '@/shared/store/sharedStore' import Home from '@/pages/home/Home' @@ -46,11 +47,46 @@ const MainRouter = () => { )}>
- } /> - } /> - } /> - } /> - } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + />
diff --git a/src/router/PrivateRoute.tsx b/src/router/PrivateRoute.tsx new file mode 100644 index 0000000..f1b9a56 --- /dev/null +++ b/src/router/PrivateRoute.tsx @@ -0,0 +1,41 @@ +import { useAuth } from '@/context/AuthContext' +import { Paths } from '@/config/Paths' +import { type FC, type ReactNode } from 'react' +import { Navigate, useLocation } from 'react-router-dom' + +type PrivateRouteProps = { + children: ReactNode + /** When true (default), user must be logged in to access the route */ + requireAuth?: boolean + /** Redirect target when auth is required but user is not logged in */ + redirectTo?: string +} + +const PrivateRoute: FC = ({ + children, + requireAuth = true, + redirectTo = Paths.home, +}) => { + const { isAuthenticated, isChecking } = useAuth() + const location = useLocation() + + if (!requireAuth) { + return children + } + + if (isChecking) { + return ( +
+
در حال بارگذاری...
+
+ ) + } + + if (!isAuthenticated) { + return + } + + return children +} + +export default PrivateRoute