/
kreyndelHam
/
Konstructorium
Обзор
Документация
Войти
/
kreyndelHam
/
Konstructorium
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
1
CI/CD
Аналитика
Безопасность
dev
frontend/src/services/api.js
477 строк
21 KB
MihahamYT
New models added
25 май 2026, 17:43
25 май 2026, 17:43
1d72a4d
Код
Авторство
О чём код?
import axios from 'axios' import { trackApiRequest } from './webAnalyticsTracker' import { getStoredAuthToken } from '../store/authStore' import { getActiveUserId } from '../utils/getActiveUserId' import { showUserError } from '../utils/showUserError' /** * Centralized HTTP client layer for all frontend-to-backend service calls. */ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '' /** * Drop empty query params to keep API contracts deterministic. * * @param {Record<string, unknown>} [params] * @returns {Record<string, unknown>} */ const normalizeQueryParams = (params = {}) => { return Object.entries(params).reduce((acc, [key, value]) => { if (value === undefined || value === null || value === '') { return acc } acc[key] = value return acc }, {}) } // Helper function to get CSRF token from cookies /** * Read CSRF token value from browser cookies. * * @returns {string|null} */ const getCsrfToken = () => { const name = 'csrftoken' let cookieValue = null if (document.cookie && document.cookie !== '') { const cookies = document.cookie.split(';') for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i].trim() if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)) break } } } return cookieValue } /** * Return whether an API error payload indicates CSRF failure. * * @param {object|undefined} errorData * @returns {boolean} */ const isCsrfErrorPayload = (errorData) => { const detail = typeof errorData?.detail === 'string' ? errorData.detail : '' return detail.includes('CSRF') || detail.includes('csrf') } /** * Refresh CSRF cookie from the target microservice (each service signs its own token). * * @param {import('axios').AxiosInstance} instance * @returns {Promise<string|null>} */ const ensureServiceCsrfToken = async (instance) => { const baseURL = (instance.defaults.baseURL || '').replace(/\/$/, '') if (!baseURL) { return getCsrfToken() } await axios.get(`${baseURL}/csrf/`, { withCredentials: true }) return getCsrfToken() } // Create axios instances for each service const authAPI = axios.create({ baseURL: `${API_BASE_URL}/api/auth`, withCredentials: true, }) const userAPI = axios.create({ baseURL: `${API_BASE_URL}/api/users`, withCredentials: true, }) const productAPI = axios.create({ baseURL: `${API_BASE_URL}/api/products`, withCredentials: true, }) const orderAPI = axios.create({ baseURL: `${API_BASE_URL}/api/orders`, withCredentials: true, }) const reviewAPI = axios.create({ baseURL: `${API_BASE_URL}/api/reviews`, withCredentials: true, }) /** Admin API HTTP client (exported for paginated list helpers). */ export const adminAPI = axios.create({ baseURL: `${API_BASE_URL}/api/admin`, withCredentials: true, }) const chatAPI = axios.create({ baseURL: `${API_BASE_URL}/api/chat`, withCredentials: true, }) // Add request interceptor for DRF token auth and per-service CSRF cookies const setupRequestInterceptors = (instance) => { instance.interceptors.request.use( async (config) => { config.metadata = { startTime: Date.now() } config._axiosInstance = instance const authToken = getStoredAuthToken() if (authToken) { config.headers.Authorization = `Token ${authToken}` } const method = config.method?.toLowerCase() const isMutating = ['post', 'put', 'patch', 'delete'].includes(method) // Token auth does not require CSRF; session-only clients warm per-service /csrf/ before mutations. if (isMutating && !authToken) { try { const csrfToken = await ensureServiceCsrfToken(instance) if (csrfToken) { config.headers['X-CSRFToken'] = csrfToken } } catch (csrfError) { console.warn('CSRF warmup skipped:', csrfError?.message || csrfError) } } return config }, (error) => Promise.reject(error), ) } // Add response interceptor to handle errors const setupResponseInterceptors = (instance, skip401Redirect = false) => { instance.interceptors.response.use( (response) => { const startTime = response.config?.metadata?.startTime if (startTime) { const url = response.config?.baseURL ? `${response.config.baseURL}${response.config.url || ''}` : response.config?.url || '' trackApiRequest({ method: response.config?.method, url, status: response.status, durationMs: Date.now() - startTime, userId: getActiveUserId(), }) } // Update CSRF token if it's in the response headers const csrfToken = response.headers['x-csrftoken'] || response.headers['X-CSRFToken'] if (csrfToken) { document.cookie = `csrftoken=${csrfToken}; path=/; SameSite=Lax` } return response }, async (error) => { const config = error.config const startTime = config?.metadata?.startTime if (startTime) { const url = config?.baseURL ? `${config.baseURL}${config.url || ''}` : config?.url || '' trackApiRequest({ method: config?.method, url, status: error.response?.status || 0, durationMs: Date.now() - startTime, userId: getActiveUserId(), }) } const status = error.response?.status const skipSilentAuthProbe = status === 401 && skip401Redirect && config?._skipAuthRedirect const axiosInstance = config?._axiosInstance if ( status === 403 && axiosInstance && !config?._csrfRetried && isCsrfErrorPayload(error.response?.data) ) { const csrfToken = await ensureServiceCsrfToken(axiosInstance) if (csrfToken) { config._csrfRetried = true config.headers = config.headers || {} config.headers['X-CSRFToken'] = csrfToken return axiosInstance.request(config) } } if (!config?.skipUserErrorToast && !skipSilentAuthProbe) { showUserError(error) } if (error.response?.status === 401) { if (!skip401Redirect && !error.config?._skipAuthRedirect) { if (window.location.pathname !== '/login' && window.location.pathname !== '/admin/login') { window.location.href = '/login' } } } else if (status === 403 && isCsrfErrorPayload(error.response?.data)) { console.error('CSRF error detected. This might be a configuration issue.') console.error('Error details:', error.response.data) } return Promise.reject(error) } ) } // Setup interceptors for all API instances const setupInterceptors = (instance, skip401Redirect = false) => { setupRequestInterceptors(instance) setupResponseInterceptors(instance, skip401Redirect) } // Setup authAPI with skip401Redirect to avoid redirect loops during auth checks setupInterceptors(authAPI, true) setupInterceptors(userAPI) setupInterceptors(productAPI) setupInterceptors(orderAPI) setupInterceptors(reviewAPI) setupInterceptors(adminAPI) setupInterceptors(chatAPI) /** Auth service API contract. */ export const authService = { requestRegistrationCode: (email) => authAPI.post('/register/email-code/request/', { email }), verifyRegistrationCode: (email, code) => authAPI.post('/register/email-code/verify/', { email, code }), register: (data) => authAPI.post('/register/', data), forgotPassword: (email) => authAPI.post('/password/forgot/', { email }), resetPassword: (data) => authAPI.post('/password/reset/', data), login: (data) => authAPI.post('/login/', data), logout: () => authAPI.post('/logout/'), fetchCsrf: () => authAPI.get('/csrf/'), getProfile: () => authAPI.get('/profile/', { skipUserErrorToast: true }), } /** User service API contract. */ export const userService = { getProfile: () => userAPI.get('/profiles/me/'), getSellers: () => userAPI.get('/sellers/'), updateProfile: (profileId, data) => userAPI.patch(`/profiles/${profileId}/`, data), /** Load recommendation survey fields (same data as in profile, dedicated endpoint). */ getSurveyState: () => userAPI.get('/profiles/survey_state/'), /** PATCH partial recommendation preferences for personalization on the home page. */ updatePreferences: (data) => userAPI.patch('/profiles/update_preferences/', data), requestTelegramVerification: (data) => userAPI.post('/profiles/request_telegram_verification/', data), unlinkTelegram: () => userAPI.post('/profiles/unlink_telegram/'), requestSellerStatus: (data) => userAPI.post('/profiles/request_seller_status/', data), submitPersonalDataRequest: (data) => userAPI.post('/legal/data-request/', data), recordRegistrationConsent: (data) => userAPI.post('/legal/registration-consent/', data), } /** Product service API contract. */ export const productService = { getProducts: (params) => productAPI.get('/models/', { params: normalizeQueryParams(params) }), getProduct: (id) => productAPI.get(`/models/${id}/`), getPersonalRecommendations: (params, config = {}) => productAPI.get('/models/personal_recommendations/', { params: normalizeQueryParams(params), ...config, }), createProduct: (data) => { const formData = new FormData() Object.keys(data).forEach((key) => { if (key === 'uploaded_images' && Array.isArray(data[key])) { data[key].forEach((file) => formData.append('uploaded_images', file)) } else if (key === 'uploaded_parts' && Array.isArray(data[key])) { data[key].forEach((file) => formData.append('uploaded_parts', file)) } else if (key === 'parts_xlsx_file' && data[key] instanceof File) { formData.append('parts_xlsx_file', data[key]) } else if (key === 'zip_file' && data[key] instanceof File) { formData.append('zip_file', data[key]) } else { const value = data[key] if (value === undefined || value === null) { return } formData.append(key, typeof value === 'object' ? JSON.stringify(value) : value) } }) return productAPI.post('/models/', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }) }, getMyProducts: (params) => productAPI.get('/models/my_models/', { params: normalizeQueryParams(params) }), updateProduct: (id, data) => productAPI.patch(`/models/${id}/`, data), incrementViews: (id) => productAPI.post(`/models/${id}/increment_views/`), getListingTypes: () => productAPI.get('/listing-types/'), getListingType: (slug) => productAPI.get(`/listing-types/${slug}/`), } /** Order service API contract. */ export const orderService = { getCart: () => orderAPI.get('/cart/'), addToCart: (cartId, data) => orderAPI.post(`/cart/${cartId}/add_item/`, data), removeFromCart: (cartId, itemId) => orderAPI.post(`/cart/${cartId}/items/${itemId}/remove/`), updateCartItemQuantity: (cartId, itemId, quantity) => orderAPI.patch(`/cart/${cartId}/items/${itemId}/update_quantity/`, { quantity }), checkout: (cartId, data) => orderAPI.post(`/cart/${cartId}/checkout/`, data), getOrders: (params) => orderAPI.get('/orders/', { params: normalizeQueryParams(params) }), getOrder: (id) => orderAPI.get(`/orders/${id}/`), getSellerOrders: (params) => orderAPI.get('/orders/seller_orders/', { params: normalizeQueryParams(params) }), updatePaymentStatus: (id, data) => orderAPI.post(`/orders/${id}/update_payment_status/`, data), updateOrderStatus: (id, data) => orderAPI.post(`/orders/${id}/update_status/`, data), downloadOrderItemPartsXlsx: (orderId, itemId) => orderAPI.get(`/orders/${orderId}/items/${itemId}/parts-xlsx/`, { responseType: 'blob', }), } /** Review service API contract. */ export const reviewService = { getReviews: (params) => reviewAPI.get('/reviews/', { params }), createReview: (data) => reviewAPI.post('/reviews/', data), getReviewsByModel: (modelId) => reviewAPI.get('/reviews/by_model/', { params: { model_3d_id: modelId } }), getReviewStats: (modelId) => reviewAPI.get('/reviews/statistics/', { params: { model_3d_id: modelId } }), getSellerReviews: (params) => reviewAPI.get('/reviews/seller_reviews/', { params: normalizeQueryParams(params) }), } /** Admin service API contract. */ export const adminService = { // Dashboard getDashboardStats: () => adminAPI.get('/dashboard/stats/'), getDashboardTimeseries: (params) => adminAPI.get('/dashboard/timeseries/', { params }), getDashboardFunnel: (params) => adminAPI.get('/dashboard/funnel/', { params }), // Users getUsers: (params) => adminAPI.get('/users/', { params: normalizeQueryParams(params) }), getUser: (id) => adminAPI.get(`/users/${id}/`), createUser: (data) => adminAPI.post('/users/', data), updateUser: (id, data) => adminAPI.patch(`/users/${id}/`, data), deleteUser: (id) => adminAPI.delete(`/users/${id}/`), toggleUserActive: (id) => adminAPI.post(`/users/${id}/toggle_active/`), toggleUserStaff: (id) => adminAPI.post(`/users/${id}/toggle_staff/`), updateUserRole: (id, role) => adminAPI.post(`/users/${id}/update_role/`, { role }), // Products getProducts: (params) => adminAPI.get('/products/', { params: normalizeQueryParams(params) }), getProduct: (id) => adminAPI.get(`/products/${id}/`), createProduct: (data) => { // axios automatically handles FormData and sets Content-Type with boundary return adminAPI.post('/products/', data) }, updateProduct: (id, data) => { // axios automatically handles FormData and sets Content-Type with boundary return adminAPI.patch(`/products/${id}/`, data) }, deleteProduct: (id) => adminAPI.delete(`/products/${id}/`), updateProductStatus: (id, status) => adminAPI.post(`/products/${id}/update_status/`, { status }), // Listing types (marketplace object constructor) getListingTypes: (params) => adminAPI.get('/listing-types/', { params: normalizeQueryParams(params) }), getListingType: (id) => adminAPI.get(`/listing-types/${id}/`), createListingType: (data) => adminAPI.post('/listing-types/', data), updateListingType: (id, data) => adminAPI.patch(`/listing-types/${id}/`, data), deleteListingType: (id) => adminAPI.delete(`/listing-types/${id}/`), getListingTypeAttributes: (listingTypeId) => adminAPI.get(`/listing-types/${listingTypeId}/attributes/`), createListingTypeAttribute: (listingTypeId, data) => adminAPI.post(`/listing-types/${listingTypeId}/attributes/`, data), getListingTypeAttributeDefinitions: (params) => adminAPI.get('/listing-type-attributes/', { params: normalizeQueryParams(params) }), updateListingTypeAttribute: (id, data) => adminAPI.patch(`/listing-type-attributes/${id}/`, data), deleteListingTypeAttribute: (id) => adminAPI.delete(`/listing-type-attributes/${id}/`), // Orders getOrders: (params) => adminAPI.get('/orders/', { params: normalizeQueryParams(params) }), getOrder: (id) => adminAPI.get(`/orders/${id}/`), createOrder: (data) => adminAPI.post('/orders/', data), updateOrder: (id, data) => adminAPI.patch(`/orders/${id}/`, data), deleteOrder: (id) => adminAPI.delete(`/orders/${id}/`), updateOrderStatus: (id, status) => adminAPI.post(`/orders/${id}/update_status/`, { status }), updateOrderPaymentStatus: (id, paymentStatus) => adminAPI.post(`/orders/${id}/update_payment_status/`, { payment_status: paymentStatus }), // Reviews getReviews: (params) => adminAPI.get('/reviews/', { params: normalizeQueryParams(params) }), getReview: (id) => adminAPI.get(`/reviews/${id}/`), createReview: (data) => adminAPI.post('/reviews/', data), updateReview: (id, data) => adminAPI.patch(`/reviews/${id}/`, data), deleteReview: (id) => adminAPI.delete(`/reviews/${id}/`), // Seller Status Requests / Notifications getSellerStatusRequests: (params) => adminAPI.get('/seller-status-requests/', { params: normalizeQueryParams(params) }), getSellerStatusRequest: (id) => adminAPI.get(`/seller-status-requests/${id}/`), approveSellerStatusRequest: (id, notes) => adminAPI.post(`/seller-status-requests/${id}/approve/`, { notes }), rejectSellerStatusRequest: (id, notes) => adminAPI.post(`/seller-status-requests/${id}/reject/`, { notes }), // Personal data requests (GDPR) getPersonalDataRequests: (params) => adminAPI.get('/personal-data-requests/', { params: normalizeQueryParams(params) }), getPersonalDataRequest: (id) => adminAPI.get(`/personal-data-requests/${id}/`), completePersonalDataAccess: (id, adminNotes) => adminAPI.post(`/personal-data-requests/${id}/complete_access/`, { admin_notes: adminNotes || '' }), completePersonalDataDelete: (id, adminNotes) => adminAPI.post(`/personal-data-requests/${id}/complete_delete/`, { admin_notes: adminNotes || '' }), completePersonalDataWithdrawAnalytics: (id, adminNotes) => adminAPI.post(`/personal-data-requests/${id}/complete_withdraw_analytics/`, { admin_notes: adminNotes || '' }), rejectPersonalDataRequest: (id, adminNotes) => adminAPI.post(`/personal-data-requests/${id}/reject/`, { admin_notes: adminNotes || '' }), // All Notifications getNotifications: (params) => adminAPI.get('/notifications/', { params: normalizeQueryParams(params) }), getNotification: (id) => adminAPI.get(`/notifications/${id}/`), markNotificationRead: (id) => adminAPI.post(`/notifications/${id}/mark_read/`), markAllNotificationsRead: () => adminAPI.post('/notifications/mark_all_read/'), getUnreadNotificationCount: () => adminAPI.get('/notifications/unread_count/'), // Chat (admin endpoints) getChatConversations: (params) => adminAPI.get('/chat/conversations/', { params: normalizeQueryParams(params) }), getChatConversation: (id) => adminAPI.get(`/chat/conversations/${id}/`), markChatConversationAsRead: (id) => adminAPI.post(`/chat/conversations/${id}/mark_as_read/`), summarizeChatConversation: (id) => adminAPI.post(`/chat/conversations/${id}/summarize/`), getChatConversationSummary: (id) => adminAPI.get(`/chat/conversations/${id}/summary/`), getChatMessages: (params) => adminAPI.get('/chat/messages/', { params: normalizeQueryParams(params) }), createChatMessage: (data) => adminAPI.post('/chat/messages/', data), deleteChatMessage: (id) => adminAPI.delete(`/chat/messages/${id}/`), // GigaChat audit logs getGigaChatLogs: (params) => adminAPI.get('/chat/gigachat-logs/', { params: normalizeQueryParams(params) }), getGigaChatLog: (id) => adminAPI.get(`/chat/gigachat-logs/${id}/`), } /** Admin ops API: service health probes and internal error journal. */ export const adminOpsService = { getServicesStatus: (params, config = {}) => adminAPI.get('/ops/services/status/', { params, ...config }), getInternalErrors: (params, config = {}) => adminAPI.get('/ops/internal-errors/', { params, ...config }), getInternalErrorsSummary: (params, config = {}) => adminAPI.get('/ops/internal-errors/summary/', { params, ...config }), resolveInternalError: (id, config = {}) => adminAPI.post(`/ops/internal-errors/${id}/resolve/`, undefined, config), } /** Focused analytics API contract for admin visualizations. */ export const adminAnalyticsService = { getDashboardStats: () => adminService.getDashboardStats(), getTimeseries: (params) => adminService.getDashboardTimeseries(params), getFunnel: (params) => adminService.getDashboardFunnel(params), getWebBundle: (params) => adminAPI.get('/dashboard/web/bundle/', { params }), getWebOverview: (params) => adminAPI.get('/dashboard/web/overview/', { params }), getWebTimeseries: (params) => adminAPI.get('/dashboard/web/timeseries/', { params }), getWebPages: (params) => adminAPI.get('/dashboard/web/pages/', { params }), getWebSources: (params) => adminAPI.get('/dashboard/web/sources/', { params }), getWebTechnology: (params) => adminAPI.get('/dashboard/web/technology/', { params }), getWebErrors: (params) => adminAPI.get('/dashboard/web/errors/', { params }), getWebApi: (params) => adminAPI.get('/dashboard/web/api/', { params }), getWebRealtime: (params) => adminAPI.get('/dashboard/web/realtime/', { params }), } /** Chat service API contract for end users. */ export const chatService = { getMyConversation: () => chatAPI.get('/conversations/my_conversation/'), getMyAiConversation: () => chatAPI.get('/conversations/my_ai_conversation/'), startNewAiConversation: () => chatAPI.post('/conversations/start_new_ai/'), resumeAiConversation: (id) => chatAPI.post(`/conversations/${id}/resume/`), getConversation: (id) => chatAPI.get(`/conversations/${id}/`), getConversationWithSeller: (params) => chatAPI.get('/conversations/with_seller/', { params: normalizeQueryParams(params) }), getSellerChats: () => chatAPI.get('/conversations/seller_chats/'), getSellerInbox: () => chatAPI.get('/conversations/seller_inbox/'), markConversationAsRead: (id) => chatAPI.post(`/conversations/${id}/mark_as_read/`), escalateAiConversation: (id) => chatAPI.post(`/conversations/${id}/escalate/`), closeAiConversation: (id) => chatAPI.post(`/conversations/${id}/close/`), submitAiFeedback: (id, data) => chatAPI.post(`/conversations/${id}/feedback/`, data), recordAiConsent: () => chatAPI.post('/ai/consent/'), getAiConsentStatus: () => chatAPI.get('/ai/consent/status/'), getMessages: (params) => chatAPI.get('/messages/', { params }), createMessage: (data) => chatAPI.post('/messages/', data), markMessageAsRead: (id) => chatAPI.post(`/messages/${id}/mark_as_read/`), }