diff --git a/src/features/auth/login/lib/useLoginForm.ts b/src/features/auth/login/lib/useLoginForm.ts index 15c6d13..a141fe6 100644 --- a/src/features/auth/login/lib/useLoginForm.ts +++ b/src/features/auth/login/lib/useLoginForm.ts @@ -8,7 +8,7 @@ import { links } from '@/shared/request/links'; import { useLoginModal } from '@/shared/zustand/auth'; import { toast } from 'react-toastify'; import { useRouter } from '@/shared/config/i18n/navigation'; -import { useUserStore } from '@/shared/zustand/user'; +import { useUserPlagiatStore } from '@/shared/zustand/user'; interface LoginData { phone: string; @@ -28,7 +28,7 @@ export function useLoginForm() { const [phone, setPhone] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); - const setUser = useUserStore((state) => state.setUser); + const setUser = useUserPlagiatStore((state) => state.setUser); const route = useRouter(); const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal); const loginReqest = useMutation({ diff --git a/src/features/auth/register/lib/useRegisterForm.ts b/src/features/auth/register/lib/useRegisterForm.ts index 0d93bff..d969094 100644 --- a/src/features/auth/register/lib/useRegisterForm.ts +++ b/src/features/auth/register/lib/useRegisterForm.ts @@ -10,12 +10,13 @@ import { links } from '@/shared/request/links'; import { toast } from 'react-toastify'; import { useRouter } from '@/shared/config/i18n/navigation'; import { AuthData } from '../../login/lib/useLoginForm'; -import { useUserStore } from '@/shared/zustand/user'; +import { useUserPlagiatStore } from '@/shared/zustand/user'; interface RegisterData { - name: string; - surname: string; + first_name: string; + last_name: string; phone: string; + password: string; } export function useRegisterForm() { @@ -23,7 +24,7 @@ export function useRegisterForm() { useRegisterZustand(); const [errors, setErrors] = useState({}); const [success, setSuccess] = useState(false); - const setUser = useUserStore((state) => state.setUser); + const setUser = useUserPlagiatStore((state) => state.setUser); const route = useRouter(); const toggleRegisterModal = useRegisterModal( (state) => state.toggleRegisterModal, @@ -88,7 +89,13 @@ export function useRegisterForm() { setErrors(validationErrors); return; } - registerRequest.mutate(registerData); + const sendedData = { + phone: registerData.phone, + first_name: registerData.name, + last_name: registerData.surname, + password: registerData.password, + }; + registerRequest.mutate(sendedData); }, [registerData, clearRegisterData], ); diff --git a/src/shared/request/apiRequest.ts b/src/shared/request/apiRequest.ts index e36482a..9d02c42 100644 --- a/src/shared/request/apiRequest.ts +++ b/src/shared/request/apiRequest.ts @@ -1,31 +1,198 @@ -import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; - +import axios, { + AxiosRequestConfig, + AxiosResponse, + AxiosError, + InternalAxiosRequestConfig, +} from 'axios'; import { getRouteLang } from './getLanguage'; +// ─── Constants ───────────────────────────────────────────────────────────────── + const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL; +const DEFAULT_LOCALE = 'uz'; // fallback locale for redirect + +// ─── Token helpers ───────────────────────────────────────────────────────────── +// Adjust key names to match whatever your auth stores them under. + +export const TokenStorage = { + getAccess: (): string | null => localStorage.getItem('access_token'), + getRefresh: (): string | null => localStorage.getItem('refresh_token'), + + setAccess: (token: string): void => + localStorage.setItem('access_token', token), + setRefresh: (token: string): void => + localStorage.setItem('refresh_token', token), + + setTokens: (access: string, refresh: string): void => { + localStorage.setItem('access_token', access); + localStorage.setItem('refresh_token', refresh); + }, + + clear: (): void => { + localStorage.removeItem('access_token'); + localStorage.removeItem('refresh_token'); + }, +}; + +// ─── Redirect helper ─────────────────────────────────────────────────────────── + +function redirectToMain(): void { + // Detect current locale from the URL path, fall back to DEFAULT_LOCALE + const pathLocale = window.location.pathname.split('/')[1]; + const validLocales = ['uz', 'ru', 'en']; + const locale = validLocales.includes(pathLocale) + ? pathLocale + : DEFAULT_LOCALE; + + window.location.href = `/${locale}`; +} + +// ─── Axios instance ──────────────────────────────────────────────────────────── const api = axios.create({ baseURL: baseUrl, + timeout: 15_000, }); +// ─── Flag to prevent multiple simultaneous refresh calls ────────────────────── + +let isRefreshing = false; + +// Queue of failed requests waiting for the new token +type FailedRequestResolver = (token: string) => void; +let failedQueue: { + resolve: FailedRequestResolver; + reject: (err: unknown) => void; +}[] = []; + +function processQueue(error: unknown, token: string | null = null): void { + failedQueue.forEach(({ resolve, reject }) => { + if (error) { + reject(error); + } else if (token) { + resolve(token); + } + }); + failedQueue = []; +} + +// ─── Request interceptor — attach access token ──────────────────────────────── + +api.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + const token = TokenStorage.getAccess(); + + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + + config.headers['Accept-Language'] = getRouteLang(); + + return config; + }, + (error: AxiosError) => Promise.reject(error), +); + +// ─── Response interceptor — handle 401, refresh, retry ─────────────────────── + +api.interceptors.response.use( + // ── 2xx: pass through unchanged ──────────────────────────────────────────── + (response: AxiosResponse) => response, + + // ── Error: attempt token refresh on 401 ──────────────────────────────────── + async (error: AxiosError) => { + const originalRequest = error.config as InternalAxiosRequestConfig & { + _retry?: boolean; + }; + + const status = error.response?.status; + + // Only attempt refresh on 401 and only once per request + if (status !== 401 || originalRequest._retry) { + return Promise.reject(error); + } + + const refreshToken = TokenStorage.getRefresh(); + + // No refresh token available — clear everything and redirect + if (!refreshToken) { + TokenStorage.clear(); + redirectToMain(); + return Promise.reject(error); + } + + // If a refresh is already in progress, queue this request + if (isRefreshing) { + return new Promise((resolve, reject) => { + failedQueue.push({ + resolve: (token: string) => { + originalRequest.headers.Authorization = `Bearer ${token}`; + resolve(api(originalRequest)); + }, + reject, + }); + }); + } + + // Mark that we're refreshing and this request has been retried + originalRequest._retry = true; + isRefreshing = true; + + try { + // ── Call your refresh endpoint ────────────────────────────────────────── + // Adjust the URL and payload shape to match your backend. + const { data } = await axios.post<{ + access_token: string; + refresh_token: string; + }>( + `${baseUrl}/auth/refresh`, // ← your refresh endpoint + { refresh_token: refreshToken }, + { headers: { 'Accept-Language': getRouteLang() } }, + ); + + const { access_token, refresh_token } = data; + + TokenStorage.setTokens(access_token, refresh_token); + + // Update the Authorization header for the retried request + originalRequest.headers.Authorization = `Bearer ${access_token}`; + + // Unblock all queued requests with the new token + processQueue(null, access_token); + + // Retry the original failed request + return api(originalRequest); + } catch (refreshError) { + // Refresh itself failed — clear tokens and redirect to home + processQueue(refreshError, null); + TokenStorage.clear(); + redirectToMain(); + return Promise.reject(refreshError); + } finally { + isRefreshing = false; + } + }, +); + +// ─── Public request function ─────────────────────────────────────────────────── + export const apiRequest = async ( method: 'GET' | 'POST' | 'PUT' | 'DELETE', url: string, data?: unknown, config?: Omit, ): Promise> => { - // ← return full response - const response: AxiosResponse = await api.request({ + const response = await api.request({ method, url, data, ...config, headers: { - 'Accept-Language': getRouteLang(), ...config?.headers, + // Accept-Language is already set in the request interceptor, + // but config?.headers can still override it if needed. }, }); - console.log(response); - return response; // ← return response, not response.data + return response; }; diff --git a/src/shared/request/links.ts b/src/shared/request/links.ts index 118fc1f..88ab53f 100644 --- a/src/shared/request/links.ts +++ b/src/shared/request/links.ts @@ -2,4 +2,5 @@ export const links = { login: '/users/login/', register: '/users/register/', plagiarismCheck: '/plagiarism/check/', + history: '/shared/documents/list/', }; diff --git a/src/shared/zustand/user.ts b/src/shared/zustand/user.ts index 26e5da9..9d9c87c 100644 --- a/src/shared/zustand/user.ts +++ b/src/shared/zustand/user.ts @@ -6,14 +6,14 @@ interface User { surname: string; } -interface UserStore { +interface UserPlagiat { user: User | null; setUser: (user: User | null) => void; clearUser: () => void; getUser: () => User | null; } -export const useUserStore = create((set, get) => ({ +export const useUserPlagiatStore = create((set, get) => ({ user: null, setUser: (user: User | null) => set({ user }), clearUser: () => set({ user: null }), diff --git a/src/widgets/fileUpload/lib/usePlagiraism.ts b/src/widgets/fileUpload/lib/usePlagiraism.ts index 39a9204..8a588a8 100644 --- a/src/widgets/fileUpload/lib/usePlagiraism.ts +++ b/src/widgets/fileUpload/lib/usePlagiraism.ts @@ -8,6 +8,8 @@ import { import { selectFullName, useUserStore } from './userStore'; import { isFormValid, validatePlagiarismForm } from './validation'; import { submitPlagiarismCheck } from '@/shared/request/plagiarismapi'; +import { toast } from 'react-toastify'; +import { useUserPlagiatStore } from '@/shared/zustand/user'; // ─── Initial States ────────────────────────────────────────────────────────── @@ -27,7 +29,7 @@ const INITIAL_SUBMISSION: SubmissionState = { export function usePlagiarismForm() { const senderFullName = useUserStore(selectFullName); - + const user = useUserPlagiatStore((state) => state.user); const [form, setForm] = useState(INITIAL_FORM); const [errors, setErrors] = useState({}); const [isPaymentOpen, setIsPaymentOpen] = useState(false); @@ -61,6 +63,11 @@ export function usePlagiarismForm() { async (e: React.FormEvent) => { e.preventDefault(); + console.log('Form submitted user:', user); // Debugging log + if (user === null) { + toast.error('Iltimos, avval tizimga kiring!'); + return; + } // Run validation first const validationErrors = validatePlagiarismForm(form); if (!isFormValid(validationErrors)) { diff --git a/src/widgets/history/ui/historyPage.tsx b/src/widgets/history/ui/historyPage.tsx index 6423a44..e42fc11 100644 --- a/src/widgets/history/ui/historyPage.tsx +++ b/src/widgets/history/ui/historyPage.tsx @@ -4,11 +4,19 @@ import { useTranslations } from 'next-intl'; import { useHistory } from '../lib/useHistory'; import { HistoryTable } from './historyTable'; import { Pagination } from './pagination'; +import { useQuery } from '@tanstack/react-query'; +import { apiRequest } from '@/shared/request/apiRequest'; +import { links } from '@/shared/request/links'; // ─── Page Header ─────────────────────────────────────────────────────────────── const PageHeader: React.FC = () => { const t = useTranslations('HistoryPage'); + const { data } = useQuery({ + queryKey: ['history'], + queryFn: () => apiRequest('GET', links.history), + }); + console.log('History data:', data); // Debugging log return (
diff --git a/src/widgets/navbar/ui/authButtons.tsx b/src/widgets/navbar/ui/authButtons.tsx index 3edb420..58d68f1 100644 --- a/src/widgets/navbar/ui/authButtons.tsx +++ b/src/widgets/navbar/ui/authButtons.tsx @@ -12,7 +12,7 @@ import SubMenuLink from './SubMenuLink'; import { ChangeLang } from './ChangeLang'; import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth'; import { useTranslations } from 'next-intl'; -import { useUserStore } from '@/shared/zustand/user'; +import { useUserPlagiatStore } from '@/shared/zustand/user'; function AuthButtons() { const t = useTranslations('Navbar'); @@ -28,23 +28,34 @@ function AuthButtons() { const toggleRegisterModal = useRegisterModal( (state) => state.toggleRegisterModal, ); - const user = useUserStore((state) => state.user); + const user = useUserPlagiatStore((state) => state.user); console.log('Current user:', user); if (user) { return ( - - - {user.name} - - {userItem.map((subItem) => ( - - - - ))} - - - +
+
+ +
+ + + + {user.name} {user.surname} + + + {userItem.map((subItem) => ( + + + + ))} + + + +
); }