user
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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<RegisterErrors>({});
|
||||
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],
|
||||
);
|
||||
|
||||
@@ -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 <T>(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||
url: string,
|
||||
data?: unknown,
|
||||
config?: Omit<AxiosRequestConfig, 'method' | 'url' | 'data'>,
|
||||
): Promise<AxiosResponse<T>> => {
|
||||
// ← return full response
|
||||
const response: AxiosResponse<T> = await api.request<T>({
|
||||
const response = await api.request<T>({
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -2,4 +2,5 @@ export const links = {
|
||||
login: '/users/login/',
|
||||
register: '/users/register/',
|
||||
plagiarismCheck: '/plagiarism/check/',
|
||||
history: '/shared/documents/list/',
|
||||
};
|
||||
|
||||
@@ -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<UserStore>((set, get) => ({
|
||||
export const useUserPlagiatStore = create<UserPlagiat>((set, get) => ({
|
||||
user: null,
|
||||
setUser: (user: User | null) => set({ user }),
|
||||
clearUser: () => set({ user: null }),
|
||||
|
||||
@@ -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<PlagiarismFormState>(INITIAL_FORM);
|
||||
const [errors, setErrors] = useState<PlagiarismFormErrors>({});
|
||||
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)) {
|
||||
|
||||
@@ -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 (
|
||||
<div className="mb-6">
|
||||
|
||||
@@ -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 (
|
||||
<NavigationMenu>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>{user.name}</NavigationMenuTrigger>
|
||||
<NavigationMenuContent className="bg-popover text-popover-foreground">
|
||||
{userItem.map((subItem) => (
|
||||
<NavigationMenuLink asChild key={subItem.title} className="w-80">
|
||||
<SubMenuLink item={subItem} />
|
||||
</NavigationMenuLink>
|
||||
))}
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenu>
|
||||
<div className="flex flex-row max-sm:items-center max-sm:justify-around gap-3">
|
||||
<div className="sm:flex hidden">
|
||||
<ChangeLang />
|
||||
</div>
|
||||
<NavigationMenu>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger className="text-xl">
|
||||
{user.name} {user.surname}
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent className="bg-popover text-popover-foreground">
|
||||
{userItem.map((subItem) => (
|
||||
<NavigationMenuLink
|
||||
asChild
|
||||
key={subItem.title}
|
||||
className="w-80"
|
||||
>
|
||||
<SubMenuLink item={subItem} />
|
||||
</NavigationMenuLink>
|
||||
))}
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user