diff --git a/backend/api/admin_api.py b/backend/api/admin_api.py index 9c77a1e..0e1f93c 100644 --- a/backend/api/admin_api.py +++ b/backend/api/admin_api.py @@ -1,6 +1,6 @@ import psycopg2 -from ..consts import DB_CONFIG +from consts import DB_CONFIG def add_product_to_shop( name: str, diff --git a/backend/api/api.py b/backend/api/api.py index 33bc059..12f75ea 100644 --- a/backend/api/api.py +++ b/backend/api/api.py @@ -1,15 +1,14 @@ import uuid import psycopg2 import psycopg2.extras -import json from datetime import timedelta from psycopg2 import IntegrityError from collections import Counter -from decimal import Decimal, InvalidOperation +from decimal import InvalidOperation -from ..utils import * -from ..type import * -from ..consts import * +from utils import * +from type import * +from consts import * def update_token_expiry_date(token: str, expiry_date: date) -> None: connection = psycopg2.connect(**DB_CONFIG) @@ -669,7 +668,10 @@ def get_products_id(token: str, table_name: str) -> list[int]: user_id: int = get_user_id(cursor, token) - cursor.execute("SELECT products_id FROM %s WHERE user_id = %s", (table_name, user_id,)) + query = sql.SQL("SELECT products_id FROM {} WHERE user_id = %s").format( + sql.Identifier(table_name) + ) + cursor.execute(query, (user_id,)) res = cursor.fetchone() if res is None or res[0] is None: diff --git a/backend/server/views.py b/backend/server/views.py index 6370fb2..48a4125 100644 --- a/backend/server/views.py +++ b/backend/server/views.py @@ -2,8 +2,8 @@ import json from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt -from backend.api import api -from backend.utils import decimal_to_float +from api import api +from utils import decimal_to_float, format_token @csrf_exempt async def shop(request): @@ -14,9 +14,9 @@ async def shop(request): user_agent = request.headers.get('User-Agent') products = decimal_to_float(products) print("get_shop", user_agent) - return JsonResponse({"OK": products}, status=200) + return JsonResponse({"success": products}, status=200) except Exception as error: - return JsonResponse({"ERROR": format(error)}, status=500) + return JsonResponse({"error": format(error)}, status=500) @csrf_exempt async def user(request): @@ -33,22 +33,22 @@ async def user(request): token = api.login(body["login"]["email"], body["login"]["password"]) elif body["unregister"]: - token = request.headers.get("Token") + token = format_token(request.headers.get("Authorization")) api.unregister(token) elif body["logout"]: - token = request.headers.get("Token") + token = format_token(request.headers.get("Authorization")) api.logout(token) elif body["add_money"]: - token = request.headers.get("Token") + token = format_token(request.headers.get("Authorization")) api.add_money(token, body["add_money"]["money"]) else: - token = request.headers.get("Token") + token = format_token(request.headers.get("Authorization")) user1 = api.get_user(token) - return JsonResponse({"OK": user1}, status=200) + return JsonResponse({"success": list(user1)}, status=200) except Exception as error: - return JsonResponse({"ERROR": format(error)}, status=500) + return JsonResponse({"error": format(error)}, status=500) @csrf_exempt @@ -56,22 +56,23 @@ async def basket(request): try: basket1 = None if request.method == 'POST': - body: dict = json.loads(request.body) - token = request.headers.get("Token") - - if body["add_product"]: - api.add_product_to_basket(token, body["add_product"]["product_id"]) - elif body["delete_product"]: - api.delete_product_from_basket(token, body["delete_product"]["product_id"]) - elif body["clear"]: - api.clear_basket(token) - elif body["buy_products"]: - api.buy_products(token) + token = format_token(request.headers.get("Authorization")) + + if request.body: + body: dict = json.loads(request.body) + if body["add_product"]: + api.add_product_to_basket(token, body["add_product"]["product_id"]) + elif body["delete_product"]: + api.delete_product_from_basket(token, body["delete_product"]["product_id"]) + elif body["clear"]: + api.clear_basket(token) + elif body["buy_products"]: + api.buy_products(token) products_id = api.get_products_id(token, "basket") basket1 = api.get_products_by_id(products_id) - return JsonResponse({"OK": basket1}, status=200) + return JsonResponse({"success": basket1}, status=200) except Exception as error: return JsonResponse({"error": format(error)}, status=500) @@ -80,9 +81,9 @@ async def history(request): try: histories = None if request.method == 'POST': - token = request.headers.get("Token") - basket1 = api.get_histories_with_products(token) - return JsonResponse({"OK": histories}, status=200) + token = format_token(request.headers.get("Authorization")) + histories = api.get_histories_with_products(token) + return JsonResponse({"success": histories}, status=200) except Exception as error: return JsonResponse({"error": format(error)}, status=500) @@ -93,7 +94,7 @@ async def login(request): if request.method == 'POST': body: dict = json.loads(request.body) token = api.login(body["email"], body["password"]) - return JsonResponse({"OK": token}, status=200) + return JsonResponse({"success": token}, status=200) except Exception as error: return JsonResponse({"error": format(error)}, status=500) @@ -102,9 +103,9 @@ async def logout(request): try: token = None if request.method == 'POST': - token = request.headers.get("Token") + token = format_token(request.headers.get("Authorization")) api.logout(token) - return JsonResponse({"OK": token}, status=200) + return JsonResponse({"success": token}, status=200) except Exception as error: return JsonResponse({"error": format(error)}, status=500) @@ -115,7 +116,7 @@ async def register(request): if request.method == 'POST': body: dict = json.loads(request.body) token = api.registration(body["nickname"], body["password"], body["email"]) - return JsonResponse({"OK": token}, status=200) + return JsonResponse({"success": token}, status=200) except Exception as error: return JsonResponse({"error": format(error)}, status=500) @@ -124,9 +125,9 @@ async def unregister(request): try: token = None if request.method == 'POST': - token = request.headers.get("Token") + token = format_token(request.headers.get("Authorization")) api.unregister(token) - return JsonResponse({"OK": token}, status=200) + return JsonResponse({"success": token}, status=200) except Exception as error: return JsonResponse({"error": format(error)}, status=500) @@ -136,9 +137,9 @@ async def add_product_to_basket(request): token = None if request.method == 'POST': body: dict = json.loads(request.body) - token = request.headers.get("Token") + token = format_token(request.headers.get("Authorization")) api.add_product_to_basket(token, body["product_id"]) - return JsonResponse({"OK": token}, status=200) + return JsonResponse({"success": token}, status=200) except Exception as error: return JsonResponse({"error": format(error)}, status=500) @@ -148,9 +149,9 @@ async def delete_product_from_basket(request): token = None if request.method == 'POST': body: dict = json.loads(request.body) - token = request.headers.get("Token") + token = format_token(request.headers.get("Authorization")) api.delete_product_from_basket(token, body["product_id"]) - return JsonResponse({"OK": token}, status=200) + return JsonResponse({"success": token}, status=200) except Exception as error: return JsonResponse({"error": format(error)}, status=500) @@ -160,9 +161,9 @@ async def buy_products(request): token = None if request.method == 'POST': body: dict = json.loads(request.body) - token = request.headers.get("Token") + token = format_token(request.headers.get("Authorization")) api.buy_products(token) - return JsonResponse({"OK": token}, status=200) + return JsonResponse({"success": token}, status=200) except Exception as error: return JsonResponse({"error": format(error)}, status=500) @@ -172,9 +173,9 @@ async def clear_basket(request): token = None if request.method == 'POST': body: dict = json.loads(request.body) - token = request.headers.get("Token") + token = format_token(request.headers.get("Authorization")) api.clear_basket(token) - return JsonResponse({"OK": token}, status=200) + return JsonResponse({"success": token}, status=200) except Exception as error: return JsonResponse({"error": format(error)}, status=500) diff --git a/backend/tests/server_api_test.py b/backend/tests/server_api_test.py index f251ebd..8760d5b 100644 --- a/backend/tests/server_api_test.py +++ b/backend/tests/server_api_test.py @@ -39,9 +39,9 @@ class TestGameShopAPI(unittest.TestCase): error_msg = get_error_message(response) raise RuntimeError(f"Registration failed ({response.status_code}): {error_msg}") data = response.json() - cls.valid_token = data.get("OK") + cls.valid_token = data.get("success") if not cls.valid_token: - raise ValueError("Response missing 'OK' field with token") + raise ValueError("Response missing 'success' field with token") uuid.UUID(cls.valid_token) except Exception as e: raise unittest.SkipTest(f"Не удалось зарегистрировать пользователя: {e}") @@ -51,7 +51,7 @@ class TestGameShopAPI(unittest.TestCase): """Удаляем пользователя после всех тестов.""" if cls.valid_token: try: - headers = {"Token": cls.valid_token} + headers = {"Authorization": "Bearer " + cls.valid_token} response = requests.post(f"{BASE_URL}/unregister/", headers=headers, timeout=10) if response.status_code != 200: error_msg = get_error_message(response) @@ -68,9 +68,9 @@ class TestGameShopAPI(unittest.TestCase): self.fail(f"GET /shop/ failed ({response.status_code}): {error_msg}") data = response.json() - self.assertIn("OK", data, f"Ответ не содержит 'OK'. Получено: {data}") - products = data["OK"] - self.assertIsInstance(products, list, f"'OK' должен быть списком, получено: {type(products)}") + self.assertIn("success", data, f"Ответ не содержит 'success'. Получено: {data}") + products = data["success"] + self.assertIsInstance(products, list, f"'success' должен быть списком, получено: {type(products)}") self.assertGreater(len(products), 0, "Список товаров пуст") product = products[0] @@ -89,31 +89,31 @@ class TestGameShopAPI(unittest.TestCase): def test_3_basket_success(self): """POST /basket/ с валидным токеном → возвращает '[]'""" - headers = {"Token": self.valid_token} + headers = {"Authorization": "Bearer " + self.valid_token} response = requests.post(f"{BASE_URL}/basket/", headers=headers) if response.status_code != 200: error_msg = get_error_message(response) self.fail(f"POST /basket/ failed ({response.status_code}): {error_msg}") data = response.json() - self.assertIn("OK", data, f"Ответ не содержит 'OK'. Получено: {data}") - self.assertEqual(data["OK"], '[]', f"Ожидалась пустая корзина ('[]'), получено: {data['OK']}") + self.assertIn("success", data, f"Ответ не содержит 'success'. Получено: {data}") + self.assertEqual(data["success"], [], f"Ожидалась пустая корзина ([]), получено: {data['success']}") def test_4_history_success(self): """POST /history/ с валидным токеном → возвращает '[]'""" - headers = {"Token": self.valid_token} + headers = {"Authorization": "Bearer " + self.valid_token} response = requests.post(f"{BASE_URL}/history/", headers=headers) if response.status_code != 200: error_msg = get_error_message(response) self.fail(f"POST /history/ failed ({response.status_code}): {error_msg}") data = response.json() - self.assertIn("OK", data, f"Ответ не содержит 'OK'. Получено: {data}") - self.assertEqual(data["OK"], '[]', f"Ожидалась пустая история ('[]'), получено: {data['OK']}") + self.assertIn("success", data, f"Ответ не содержит 'success'. Получено: {data}") + self.assertEqual(data["success"], [], f"Ожидалась пустая история ([]), получено: {data['success']}") def test_5_basket_invalid_token(self): """POST /basket/ с невалидным токеном → ошибка 500""" - headers = {"Token": "invalid-token-12345"} + headers = {"Authorization": "Bearer " + "invalid-token-12345"} response = requests.post(f"{BASE_URL}/basket/", headers=headers) if response.status_code != 500: error_msg = get_error_message(response) @@ -126,7 +126,7 @@ class TestGameShopAPI(unittest.TestCase): def test_6_history_invalid_token(self): """POST /history/ с невалидным токеном → ошибка 500""" - headers = {"Token": str(uuid.uuid4())} + headers = {"Authorization": "Bearer " + "invalid-token-12345"} response = requests.post(f"{BASE_URL}/history/", headers=headers) if response.status_code != 500: error_msg = get_error_message(response) @@ -147,8 +147,8 @@ class TestGameShopAPI(unittest.TestCase): self.fail(f"POST /login/ failed ({response.status_code}): {error_msg}") data = response.json() - self.assertIn("OK", data, f"Ответ не содержит 'OK'. Получено: {data}") - login_token = data["OK"] + self.assertIn("success", data, f"Ответ не содержит 'OK'. Получено: {data}") + login_token = data["success"] try: uuid.UUID(login_token) @@ -156,22 +156,22 @@ class TestGameShopAPI(unittest.TestCase): self.fail(f"Токен от /login/ не является валидным UUID: {login_token}") # Проверка, что токен работает - basket_resp = requests.post(f"{BASE_URL}/basket/", headers={"Token": login_token}) + basket_resp = requests.post(f"{BASE_URL}/basket/", headers={"Authorization": "Bearer " + login_token}) if basket_resp.status_code != 200: error_msg = get_error_message(basket_resp) self.fail(f"Токен от /login/ не работает: {error_msg}") def test_8_logout_success(self): """POST /logout/ с валидным токеном — успешный выход""" - headers = {"Token": self.valid_token} + headers = {"Authorization": "Bearer " + self.valid_token} response = requests.post(f"{BASE_URL}/logout/", headers=headers) if response.status_code != 200: error_msg = get_error_message(response) self.fail(f"POST /logout/ failed ({response.status_code}): {error_msg}") data = response.json() - self.assertIn("OK", data, f"Ответ не содержит 'OK'. Получено: {data}") - self.assertEqual(data["OK"], self.valid_token, "В ответе должен быть тот же токен") + self.assertIn("success", data, f"Ответ не содержит 'OK'. Получено: {data}") + self.assertEqual(data["success"], self.valid_token, "В ответе должен быть тот же токен") def test_9_all_scenarios_completed(self): """Финальная проверка: все тесты пройдены""" diff --git a/backend/utils/utils.py b/backend/utils/utils.py index b2636bc..ad0af43 100644 --- a/backend/utils/utils.py +++ b/backend/utils/utils.py @@ -56,3 +56,6 @@ def decimal_to_float(data: list[dict]): for item in data ] return clean_data + +def format_token(token: str) -> str: + return token[7:] # delete "Bearer " diff --git a/frontend/shop/src/network.ts b/frontend/shop/src/network.ts index 55bde77..d1128b4 100644 --- a/frontend/shop/src/network.ts +++ b/frontend/shop/src/network.ts @@ -1,37 +1,24 @@ -import type { Product } from './types' +const DEV_URL = '/dev_api' +const PROD_URL = 'https://shop.softwarrior.ru:8090' export interface NetworkApi { - getProducts: () => Product[] + getStringURL: (suffix: string) => string } class Network implements NetworkApi { - getProducts(): Product[] { - return [ - { - id: 2, - name: 'Серебрянный паровоз', - count: 23, - reserved: 0, - picture_url: - 'https://git.softwarrior.ru/stepik104/game_shop/raw/branch/main/images/silver_train.png', - description: - 'Серебро - благородный метал, такой паровоз не стыдно выкатить на рельсы', - type: 'skin', - cost: '50.00', - }, - { - id: 4, - name: 'Золотая башня', - count: 10, - reserved: 0, - picture_url: - 'https://git.softwarrior.ru/stepik104/game_shop/raw/branch/main/images/golden_tower.png', - description: - 'Целая башня из чистого золота, кто-то скажет - непрактично, но мы ответим - да', - type: 'skin', - cost: '100.00', - }, - ] + private baseUrl: string + + constructor() { + if (import.meta.env.DEV) { + this.baseUrl = DEV_URL + } else { + this.baseUrl = PROD_URL + } + } + + getStringURL(suffix: string): string { + const urlString = `${this.baseUrl}/${suffix.replace(/^\//, '')}` + return urlString } } diff --git a/frontend/shop/src/pages/shop/shop-slice.ts b/frontend/shop/src/pages/shop/shop-slice.ts index bb88e0b..931f4f8 100644 --- a/frontend/shop/src/pages/shop/shop-slice.ts +++ b/frontend/shop/src/pages/shop/shop-slice.ts @@ -7,14 +7,21 @@ interface AddBasketAction { count: number } +type ResponseData = { + error?: string + success?: Array +} + interface ShopState { products: Product[] loading: boolean + error?: string } const initialState: ShopState = { products: [], loading: false, + error: undefined, } export const shopSlice = createSlice({ @@ -31,10 +38,19 @@ export const shopSlice = createSlice({ builder .addCase(fetchProducts.pending, state => { state.loading = true + state.error = undefined }) .addCase(fetchProducts.fulfilled, (state, action) => { - state.products = action.payload + state.products = action.payload.success as Product[] + state.loading = false + }) + .addCase(fetchProducts.rejected, (state, action) => { state.loading = false + if (action.payload) { + state.error = (action.payload as ResponseData).error + } else { + state.error = action.error.message + } }) }, }) @@ -45,10 +61,28 @@ export const selectProducts = (state: RootState) => state.shop.products export const shopReducer = shopSlice.reducer -export const fetchProducts = createAsyncThunk( - 'shop/fetchProducts', +export const fetchProducts = createAsyncThunk( + 'products/fetchProducts', async (_, { extra: networkApi }) => { - const response = await networkApi.getProducts() - return response + try { + const response = await fetch(networkApi.getStringURL('shop/'), { + headers: { + //Authorization: `Bearer ${localStorage.getItem('token')}`, + //'Content-Type': 'application/json', + }, + mode: 'cors', + }) + + if (!response.ok) { + throw new Error('Server error!') + } + + const responseData: ResponseData = await response.json() + return responseData + } catch (error) { + return { + error: error instanceof Error ? error.message : 'Unknown error', + } + } } ) diff --git a/frontend/shop/src/pages/shop/shop.module.css b/frontend/shop/src/pages/shop/shop.module.css deleted file mode 100644 index 2c704af..0000000 --- a/frontend/shop/src/pages/shop/shop.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.shop { - color: red; -} diff --git a/frontend/shop/src/pages/shop/shop.tsx b/frontend/shop/src/pages/shop/shop.tsx index 93e2a1e..8015f8a 100644 --- a/frontend/shop/src/pages/shop/shop.tsx +++ b/frontend/shop/src/pages/shop/shop.tsx @@ -1,6 +1,5 @@ import { memo, useCallback, useEffect } from 'react' -import styles from './shop.module.css' -import { Box, Button } from '@mui/material' +import { Box, Button, Typography } from '@mui/material' import { useAppDispatch, useAppSelector } from '../../hooks' import { addToBasket, fetchProducts, selectProducts } from './shop-slice' @@ -18,11 +17,10 @@ export const Shop = memo(() => { return ( - {'Магазин'} - {JSON.stringify(products)} + {JSON.stringify(products)} ) }) diff --git a/frontend/shop/vite.config.ts b/frontend/shop/vite.config.ts index e878765..9f46470 100644 --- a/frontend/shop/vite.config.ts +++ b/frontend/shop/vite.config.ts @@ -4,4 +4,30 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + server: { + port: 5173, + proxy: { + '/dev_api': { + target: 'http://127.0.0.1:8090', + changeOrigin: true, + secure: false, + rewrite: path => path.replace(/^\/dev_api/, ''), + configure: (proxy, _options) => { + proxy.on('error', (err, _req, _res) => { + console.log('Proxy error:', err) + }) + proxy.on('proxyReq', (proxyReq, req, _res) => { + console.log( + proxyReq, + 'Proxying:', + req.method, + req.url, + '→', + 'http://127.0.0.1:8090' + req?.url?.replace('/dev_api', '') + ) + }) + }, + }, + }, + }, })