user
This commit is contained in:
@@ -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 }),
|
||||
|
||||
Reference in New Issue
Block a user