user
This commit is contained in:
@@ -8,7 +8,7 @@ import { links } from '@/shared/request/links';
|
|||||||
import { useLoginModal } from '@/shared/zustand/auth';
|
import { useLoginModal } from '@/shared/zustand/auth';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||||
import { useUserStore } from '@/shared/zustand/user';
|
import { useUserPlagiatStore } from '@/shared/zustand/user';
|
||||||
|
|
||||||
interface LoginData {
|
interface LoginData {
|
||||||
phone: string;
|
phone: string;
|
||||||
@@ -28,7 +28,7 @@ export function useLoginForm() {
|
|||||||
const [phone, setPhone] = useState('');
|
const [phone, setPhone] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const setUser = useUserStore((state) => state.setUser);
|
const setUser = useUserPlagiatStore((state) => state.setUser);
|
||||||
const route = useRouter();
|
const route = useRouter();
|
||||||
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
|
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
|
||||||
const loginReqest = useMutation({
|
const loginReqest = useMutation({
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import { links } from '@/shared/request/links';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||||
import { AuthData } from '../../login/lib/useLoginForm';
|
import { AuthData } from '../../login/lib/useLoginForm';
|
||||||
import { useUserStore } from '@/shared/zustand/user';
|
import { useUserPlagiatStore } from '@/shared/zustand/user';
|
||||||
|
|
||||||
interface RegisterData {
|
interface RegisterData {
|
||||||
name: string;
|
first_name: string;
|
||||||
surname: string;
|
last_name: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRegisterForm() {
|
export function useRegisterForm() {
|
||||||
@@ -23,7 +24,7 @@ export function useRegisterForm() {
|
|||||||
useRegisterZustand();
|
useRegisterZustand();
|
||||||
const [errors, setErrors] = useState<RegisterErrors>({});
|
const [errors, setErrors] = useState<RegisterErrors>({});
|
||||||
const [success, setSuccess] = useState(false);
|
const [success, setSuccess] = useState(false);
|
||||||
const setUser = useUserStore((state) => state.setUser);
|
const setUser = useUserPlagiatStore((state) => state.setUser);
|
||||||
const route = useRouter();
|
const route = useRouter();
|
||||||
const toggleRegisterModal = useRegisterModal(
|
const toggleRegisterModal = useRegisterModal(
|
||||||
(state) => state.toggleRegisterModal,
|
(state) => state.toggleRegisterModal,
|
||||||
@@ -88,7 +89,13 @@ export function useRegisterForm() {
|
|||||||
setErrors(validationErrors);
|
setErrors(validationErrors);
|
||||||
return;
|
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],
|
[registerData, clearRegisterData],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,31 +1,198 @@
|
|||||||
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
|
import axios, {
|
||||||
|
AxiosRequestConfig,
|
||||||
|
AxiosResponse,
|
||||||
|
AxiosError,
|
||||||
|
InternalAxiosRequestConfig,
|
||||||
|
} from 'axios';
|
||||||
import { getRouteLang } from './getLanguage';
|
import { getRouteLang } from './getLanguage';
|
||||||
|
|
||||||
|
// ─── Constants ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
|
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({
|
const api = axios.create({
|
||||||
baseURL: baseUrl,
|
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>(
|
export const apiRequest = async <T>(
|
||||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||||
url: string,
|
url: string,
|
||||||
data?: unknown,
|
data?: unknown,
|
||||||
config?: Omit<AxiosRequestConfig, 'method' | 'url' | 'data'>,
|
config?: Omit<AxiosRequestConfig, 'method' | 'url' | 'data'>,
|
||||||
): Promise<AxiosResponse<T>> => {
|
): Promise<AxiosResponse<T>> => {
|
||||||
// ← return full response
|
const response = await api.request<T>({
|
||||||
const response: AxiosResponse<T> = await api.request<T>({
|
|
||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
data,
|
data,
|
||||||
...config,
|
...config,
|
||||||
headers: {
|
headers: {
|
||||||
'Accept-Language': getRouteLang(),
|
|
||||||
...config?.headers,
|
...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/',
|
login: '/users/login/',
|
||||||
register: '/users/register/',
|
register: '/users/register/',
|
||||||
plagiarismCheck: '/plagiarism/check/',
|
plagiarismCheck: '/plagiarism/check/',
|
||||||
|
history: '/shared/documents/list/',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ interface User {
|
|||||||
surname: string;
|
surname: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserStore {
|
interface UserPlagiat {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
setUser: (user: User | null) => void;
|
setUser: (user: User | null) => void;
|
||||||
clearUser: () => void;
|
clearUser: () => void;
|
||||||
getUser: () => User | null;
|
getUser: () => User | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUserStore = create<UserStore>((set, get) => ({
|
export const useUserPlagiatStore = create<UserPlagiat>((set, get) => ({
|
||||||
user: null,
|
user: null,
|
||||||
setUser: (user: User | null) => set({ user }),
|
setUser: (user: User | null) => set({ user }),
|
||||||
clearUser: () => set({ user: null }),
|
clearUser: () => set({ user: null }),
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
import { selectFullName, useUserStore } from './userStore';
|
import { selectFullName, useUserStore } from './userStore';
|
||||||
import { isFormValid, validatePlagiarismForm } from './validation';
|
import { isFormValid, validatePlagiarismForm } from './validation';
|
||||||
import { submitPlagiarismCheck } from '@/shared/request/plagiarismapi';
|
import { submitPlagiarismCheck } from '@/shared/request/plagiarismapi';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { useUserPlagiatStore } from '@/shared/zustand/user';
|
||||||
|
|
||||||
// ─── Initial States ──────────────────────────────────────────────────────────
|
// ─── Initial States ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -27,7 +29,7 @@ const INITIAL_SUBMISSION: SubmissionState = {
|
|||||||
|
|
||||||
export function usePlagiarismForm() {
|
export function usePlagiarismForm() {
|
||||||
const senderFullName = useUserStore(selectFullName);
|
const senderFullName = useUserStore(selectFullName);
|
||||||
|
const user = useUserPlagiatStore((state) => state.user);
|
||||||
const [form, setForm] = useState<PlagiarismFormState>(INITIAL_FORM);
|
const [form, setForm] = useState<PlagiarismFormState>(INITIAL_FORM);
|
||||||
const [errors, setErrors] = useState<PlagiarismFormErrors>({});
|
const [errors, setErrors] = useState<PlagiarismFormErrors>({});
|
||||||
const [isPaymentOpen, setIsPaymentOpen] = useState(false);
|
const [isPaymentOpen, setIsPaymentOpen] = useState(false);
|
||||||
@@ -61,6 +63,11 @@ export function usePlagiarismForm() {
|
|||||||
async (e: React.FormEvent) => {
|
async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
console.log('Form submitted user:', user); // Debugging log
|
||||||
|
if (user === null) {
|
||||||
|
toast.error('Iltimos, avval tizimga kiring!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Run validation first
|
// Run validation first
|
||||||
const validationErrors = validatePlagiarismForm(form);
|
const validationErrors = validatePlagiarismForm(form);
|
||||||
if (!isFormValid(validationErrors)) {
|
if (!isFormValid(validationErrors)) {
|
||||||
|
|||||||
@@ -4,11 +4,19 @@ import { useTranslations } from 'next-intl';
|
|||||||
import { useHistory } from '../lib/useHistory';
|
import { useHistory } from '../lib/useHistory';
|
||||||
import { HistoryTable } from './historyTable';
|
import { HistoryTable } from './historyTable';
|
||||||
import { Pagination } from './pagination';
|
import { Pagination } from './pagination';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { apiRequest } from '@/shared/request/apiRequest';
|
||||||
|
import { links } from '@/shared/request/links';
|
||||||
|
|
||||||
// ─── Page Header ───────────────────────────────────────────────────────────────
|
// ─── Page Header ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const PageHeader: React.FC = () => {
|
const PageHeader: React.FC = () => {
|
||||||
const t = useTranslations('HistoryPage');
|
const t = useTranslations('HistoryPage');
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ['history'],
|
||||||
|
queryFn: () => apiRequest('GET', links.history),
|
||||||
|
});
|
||||||
|
console.log('History data:', data); // Debugging log
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import SubMenuLink from './SubMenuLink';
|
|||||||
import { ChangeLang } from './ChangeLang';
|
import { ChangeLang } from './ChangeLang';
|
||||||
import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth';
|
import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useUserStore } from '@/shared/zustand/user';
|
import { useUserPlagiatStore } from '@/shared/zustand/user';
|
||||||
|
|
||||||
function AuthButtons() {
|
function AuthButtons() {
|
||||||
const t = useTranslations('Navbar');
|
const t = useTranslations('Navbar');
|
||||||
@@ -28,23 +28,34 @@ function AuthButtons() {
|
|||||||
const toggleRegisterModal = useRegisterModal(
|
const toggleRegisterModal = useRegisterModal(
|
||||||
(state) => state.toggleRegisterModal,
|
(state) => state.toggleRegisterModal,
|
||||||
);
|
);
|
||||||
const user = useUserStore((state) => state.user);
|
const user = useUserPlagiatStore((state) => state.user);
|
||||||
console.log('Current user:', user);
|
console.log('Current user:', user);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
return (
|
return (
|
||||||
<NavigationMenu>
|
<div className="flex flex-row max-sm:items-center max-sm:justify-around gap-3">
|
||||||
<NavigationMenuItem>
|
<div className="sm:flex hidden">
|
||||||
<NavigationMenuTrigger>{user.name}</NavigationMenuTrigger>
|
<ChangeLang />
|
||||||
<NavigationMenuContent className="bg-popover text-popover-foreground">
|
</div>
|
||||||
{userItem.map((subItem) => (
|
<NavigationMenu>
|
||||||
<NavigationMenuLink asChild key={subItem.title} className="w-80">
|
<NavigationMenuItem>
|
||||||
<SubMenuLink item={subItem} />
|
<NavigationMenuTrigger className="text-xl">
|
||||||
</NavigationMenuLink>
|
{user.name} {user.surname}
|
||||||
))}
|
</NavigationMenuTrigger>
|
||||||
</NavigationMenuContent>
|
<NavigationMenuContent className="bg-popover text-popover-foreground">
|
||||||
</NavigationMenuItem>
|
{userItem.map((subItem) => (
|
||||||
</NavigationMenu>
|
<NavigationMenuLink
|
||||||
|
asChild
|
||||||
|
key={subItem.title}
|
||||||
|
className="w-80"
|
||||||
|
>
|
||||||
|
<SubMenuLink item={subItem} />
|
||||||
|
</NavigationMenuLink>
|
||||||
|
))}
|
||||||
|
</NavigationMenuContent>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
</NavigationMenu>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user