diff --git a/backend/database.py b/backend/database.py index 1f22ca8..d85e5df 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1,9 +1,8 @@ # app/database.py import sys -from sqlalchemy import event - from constants import DATABASE_URL, TEST_DATABASE_URL +from sqlalchemy import event from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker diff --git a/backend/main.py b/backend/main.py index 340e96e..ab793ea 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2,7 +2,7 @@ from database import create_tables from fastapi import FastAPI -from routes import good, good_category, login +from routes import good, good_category, login, payment, recipient @asynccontextmanager @@ -23,10 +23,12 @@ async def lifespan(_: FastAPI): app.include_router(good_category.router, prefix="/api/v1/good-categories", tags=["Категории товаров"]) app.include_router(good.router, prefix="/api/v1/goods", tags=["Товары"]) - +app.include_router(payment.router, prefix="/api/v1/payments", tags=["Методы оплаты"]) +app.include_router(recipient.router, prefix="/api/v1/recipients", tags=["Получатели"]) app.include_router(login.router, prefix="/api/v1/auth", tags=["Авторизация"]) + @app.get("/") async def root(): return {"detail": "Welcome to the API! Go to /docs to see the documentation."} diff --git a/backend/models/basket.py b/backend/models/basket.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/models/checkout.py b/backend/models/checkout.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/models/delivery.py b/backend/models/delivery.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/models/payment.py b/backend/models/payment.py new file mode 100644 index 0000000..a4ed0c0 --- /dev/null +++ b/backend/models/payment.py @@ -0,0 +1,12 @@ +from database import Base +from sqlalchemy import Column, Integer, LargeBinary, String, Text + + +class PaymentMethods(Base): + __tablename__ = "payment_methods" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + image = Column(String, nullable=True) + diff --git a/backend/models/recipient.py b/backend/models/recipient.py new file mode 100644 index 0000000..f9884f8 --- /dev/null +++ b/backend/models/recipient.py @@ -0,0 +1,16 @@ +from database import Base +from sqlalchemy import Column, ForeignKey, Integer, String, Text + + +class Recipients(Base): + __tablename__ = "recipients" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE')) + first_name = Column(String(50), nullable=False) + last_name = Column(String(50), nullable=True) + middle_name = Column(String(50), nullable=True) + address = Column(String(250), nullable=False) + zipcode = Column(String(50), nullable=True) + phone = Column(String(50), nullable=False) + email = Column(String(50), nullable=True) diff --git a/backend/models/transaction.py b/backend/models/transaction.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/models/user.py b/backend/models/user.py index 6e949f5..c6bc63d 100644 --- a/backend/models/user.py +++ b/backend/models/user.py @@ -1,9 +1,8 @@ from datetime import datetime +from enum import Enum as PyEnum from database import Base -from sqlalchemy import Column, Integer, String, DateTime, Enum - -from enum import Enum as PyEnum +from sqlalchemy import Column, DateTime, Enum, Integer, String class UserRole(PyEnum): diff --git a/backend/routes/good.py b/backend/routes/good.py index 51cc806..c1ffc25 100644 --- a/backend/routes/good.py +++ b/backend/routes/good.py @@ -1,4 +1,3 @@ -from utils.auth import verify_token from constants import GOOD_PAGE_SIZE as PAGE_SIZE from database import get_db from fastapi import APIRouter, Depends, HTTPException, Query, Request @@ -6,7 +5,7 @@ from schemas import GoodCreate, GoodModel from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession - +from utils.auth import verify_token from validators import validate_category_exists router = APIRouter() diff --git a/backend/routes/good_category.py b/backend/routes/good_category.py index 23c85f8..ed689ac 100644 --- a/backend/routes/good_category.py +++ b/backend/routes/good_category.py @@ -1,5 +1,4 @@ # app/routes/good_category.py -from utils.auth import verify_token from constants import GOOD_CATEGORY_PAGE_SIZE as PAGE_SIZE from database import get_db from fastapi import APIRouter, Depends, HTTPException, Query, Request @@ -7,6 +6,7 @@ from schemas import GoodCategoryCreate, GoodCategoryModel from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession +from utils.auth import verify_token from validators import (validate_category_exists, validate_category_name, validate_category_name_update, validate_parent_category, validate_parent_itself) diff --git a/backend/routes/login.py b/backend/routes/login.py index 2a40de0..fd26322 100644 --- a/backend/routes/login.py +++ b/backend/routes/login.py @@ -1,34 +1,39 @@ # app/routes/login.py from datetime import datetime, timedelta +from typing import Optional +import jwt +from constants import ALGORITHM, SECRET_KEY +from database import get_db, is_testing +from fastapi import (APIRouter, Cookie, Depends, Header, HTTPException, + Request, Response) +from fastapi.security import HTTPAuthorizationCredentials +from models.user import OTP, User +from pydantic.v1 import NotNoneError +from schemas import UserCreate, UserLogin, UserVerify from sqlalchemy import select - -from database import get_db -from fastapi import APIRouter, Depends, HTTPException, Response -from schemas import UserLogin, UserVerify from sqlalchemy.ext.asyncio import AsyncSession -from database import is_testing - -from models.user import OTP -from utils.auth import send_verification_email, generate_code, generate_access_token, generate_refresh_token +from utils.auth import (generate_access_token, generate_code, + generate_refresh_token, security, + send_verification_email) router = APIRouter() @router.post("/login") -async def login(user_login: UserLogin, db: AsyncSession = Depends(get_db)): +async def login(user_login: UserLogin, + db: AsyncSession = Depends(get_db)): """Вход пользователя""" otp = generate_code() expiration = datetime.now() + timedelta(minutes=5) - - db_otp = OTP(email=user_login.email, otp=otp, expiration=expiration) + db_otp = OTP(email=str(user_login.email), otp=otp, expiration=expiration) db.add(db_otp) await db.commit() if is_testing: return {"otp": otp} - send_verification_email(user_login.email, "Код для входа в аккаунт", otp) + send_verification_email(str(user_login.email), "Код для входа в аккаунт", otp) return {"message": "OTP sent to your email."} @@ -61,5 +66,17 @@ async def confirm(user_verify: UserVerify, response: Response, db: AsyncSession samesite="strict", # Ограничьте доступ к этому домену ) + result = await db.execute(select(User).filter(User.email == user_verify.email)) + db_user = result.scalar_one_or_none() + + if not db_user: + db_user = User(email=str(user_verify.email), + created_at=datetime.utcnow()) + db.add(db_user) + db_user.last_login = datetime.utcnow() + + await db.commit() + await db.refresh(db_user) + # Возврат access-токена return {"access_token": access_token} diff --git a/backend/routes/payment.py b/backend/routes/payment.py new file mode 100644 index 0000000..62b7805 --- /dev/null +++ b/backend/routes/payment.py @@ -0,0 +1,71 @@ +from database import get_db +from fastapi import APIRouter, Depends, HTTPException +from models.payment import PaymentMethods +from schemas import PaymentMethodCreate, PaymentMethodModel +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from utils.auth import verify_token + +router = APIRouter() + +@router.get("") +async def get_payments(db: AsyncSession = Depends(get_db)): + """Получение списка методов оплаты""" + payments = (await db.execute(select(PaymentMethods))).scalars().all() + + return { + "items": [PaymentMethodModel.model_validate(payment) for payment in payments], + } + + +@router.post("", response_model=PaymentMethodModel) +async def create_payment(payment: PaymentMethodCreate, db: AsyncSession = Depends(get_db), + user_data: dict = Depends(verify_token)): + """Добавление метода оплаты""" + + db_payment = PaymentMethods(**payment.model_dump()) + db.add(db_payment) + await db.commit() + await db.refresh(db_payment) + + return PaymentMethodModel.model_validate(db_payment) + + +@router.get("/{payment_id}", response_model=PaymentMethodModel) +async def get_payment(payment_id: int, db: AsyncSession = Depends(get_db)): + """Получение метода оплаты по идентификатору""" + payment = await db.get(PaymentMethods, payment_id) + if payment is None: + raise HTTPException(status_code=404, detail="Payment method not found") + + return PaymentMethodModel.model_validate(payment) + + +@router.patch("/{payment_id}", response_model=PaymentMethodModel) +async def update_payment(payment_id: int, payment: PaymentMethodCreate, db: AsyncSession = Depends(get_db), + user_data: dict = Depends(verify_token)): + """Обновление товара""" + db_payment = await db.get(PaymentMethods, payment_id) + if db_payment is None: + raise HTTPException(status_code=404, detail="Payment method not found") + + for key, value in payment.model_dump().items(): + setattr(db_payment, key, value) + + await db.commit() + await db.refresh(db_payment) + + return PaymentMethodModel.model_validate(db_payment) + + +@router.delete("/{payment_id}") +async def delete_good(payment_id: int, db: AsyncSession = Depends(get_db), + user_data: dict = Depends(verify_token)): + """Удаление товара""" + db_payment = await db.get(PaymentMethods, payment_id) + if db_payment is None: + raise HTTPException(status_code=404, detail="Payment method not found") + + await db.delete(db_payment) + await db.commit() + return {"detail": "Payment method deleted"} diff --git a/backend/routes/recipient.py b/backend/routes/recipient.py new file mode 100644 index 0000000..01bfaf3 --- /dev/null +++ b/backend/routes/recipient.py @@ -0,0 +1,124 @@ +from database import get_db +from fastapi import APIRouter, Depends, HTTPException +from models.recipient import Recipients +from models.user import User, UserRole +from schemas import RecipientCreate, RecipientEdit, RecipientModel +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from utils.auth import verify_token + +router = APIRouter() + + +@router.get("") +async def get_recipients(db: AsyncSession = Depends(get_db), + user_data: dict = Depends(verify_token)): + """Получение всех получателей, если запрос делает админ (или продавец), и только своих, если - пользователь""" + user_email = user_data.get("sub") + result = await db.execute(select(User).filter(User.email == user_email)) + db_user = result.scalars().first() + + if db_user.role in [UserRole.ADMIN, UserRole.SELLER]: + result_recipients = await db.execute(select(Recipients)) + else: + result_recipients = await db.execute( + select(Recipients).filter(Recipients.user_id == db_user.id) + ) + + recipients = result_recipients.scalars().all() + + return { + "items": [RecipientModel.model_validate(recipient) for recipient in recipients], + } + + +@router.post("", response_model=RecipientModel) +async def add_recipients(payment: RecipientCreate, db: AsyncSession = Depends(get_db), + user_data: dict = Depends(verify_token)): + """Добавление получателя""" + user_email = user_data.get("sub") + result = await db.execute(select(User).filter(User.email == user_email)) + db_user = result.scalars().first() + + if db_user.role in [UserRole.ADMIN, UserRole.SELLER]: + user_id = payment.user_id + else: + user_id = db_user.id + + new_recipient = Recipients( + user_id=user_id, + first_name=payment.first_name, + last_name=payment.last_name, + middle_name=payment.middle_name, + address=payment.address, + zipcode=payment.zipcode, + phone=payment.phone, + email=str(payment.email), + ) + + db.add(new_recipient) + await db.commit() + await db.refresh(new_recipient) + + return RecipientModel.model_validate(new_recipient) + + +@router.get("/{recipient_id}", response_model=RecipientModel) +async def get_payment(recipient_id: int, db: AsyncSession = Depends(get_db), + user_data: dict = Depends(verify_token)): + """Получение метода оплаты по идентификатору""" + recipient = await db.get(Recipients, recipient_id) + if recipient is None: + raise HTTPException(status_code=404, detail="Recipient not found") + + user_email = user_data.get("sub") + result = await db.execute(select(User).filter(User.email == user_email)) + db_user = result.scalars().first() + + if recipient.user_id != db_user.id and db_user.role not in [UserRole.ADMIN, UserRole.SELLER]: + raise HTTPException(status_code=404, detail="Recipient not found") + return RecipientModel.model_validate(recipient) + + +@router.patch("/{recipient_id}", response_model=RecipientModel) +async def update_recipient(recipient_id: int, recipient: RecipientEdit, db: AsyncSession = Depends(get_db), + user_data: dict = Depends(verify_token)): + """Обновление товара""" + db_recipient = await db.get(Recipients, recipient_id) + if db_recipient is None: + raise HTTPException(status_code=404, detail="Recipient not found") + + user_email = user_data.get("sub") + result = await db.execute(select(User).filter(User.email == user_email)) + db_user = result.scalars().first() + + if db_recipient.user_id != db_user.id and db_user.role not in [UserRole.ADMIN, UserRole.SELLER]: + raise HTTPException(status_code=404, detail="Recipient not found") + + for key, value in recipient.model_dump().items(): + setattr(db_recipient, key, value) + + await db.commit() + await db.refresh(db_recipient) + + return RecipientModel.model_validate(db_recipient) + + +@router.delete("/{recipient_id}") +async def delete_recipient(recipient_id: int, db: AsyncSession = Depends(get_db), + user_data: dict = Depends(verify_token)): + """Удаление товара""" + db_recipient = await db.get(Recipients, recipient_id) + if db_recipient is None: + raise HTTPException(status_code=404, detail="Recipient not found") + + user_email = user_data.get("sub") + result = await db.execute(select(User).filter(User.email == user_email)) + db_user = result.scalars().first() + + if db_recipient.user_id != db_user.id and db_user.role not in [UserRole.ADMIN, UserRole.SELLER]: + raise HTTPException(status_code=404, detail="Recipient not found") + + await db.delete(db_recipient) + await db.commit() + return {"detail": "Recipient deleted"} diff --git a/backend/schemas.py b/backend/schemas.py index 0220f97..69dfef6 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -1,7 +1,8 @@ # app/schemas.py +from datetime import datetime from typing import Optional -from pydantic import BaseModel, EmailStr +from pydantic import BaseModel, EmailStr, Field class GoodCategoryCreate(BaseModel): @@ -16,9 +17,6 @@ class Config: class GoodCategoryModel(GoodCategoryCreate): id: int - name: str - description: Optional[str] = None - parent_id: Optional[int] = None class Config: orm_mode = True @@ -38,10 +36,6 @@ class Config: class GoodModel(GoodCreate): id: int - name: str - description: Optional[str] = None - price: int - category_id: Optional[int] = None class Config: orm_mode = True @@ -57,10 +51,67 @@ class UserVerify(BaseModel): otp: str -class UserRead(BaseModel): - id: int +class UserCreate(BaseModel): email: EmailStr - is_verified: bool + created_at: datetime | None = None + last_login: datetime | None = None + role: str | None = Field(default="user") + + class Config: + orm_mode = True + from_attributes = True + + +class PaymentMethodCreate(BaseModel): + title: str + description: Optional[str] = None + image: Optional[str] = None + + class Config: + orm_mode = True + from_attributes = True + + +class PaymentMethodModel(PaymentMethodCreate): + id: int class Config: orm_mode = True + from_attributes = True + + +class RecipientEdit(BaseModel): + first_name: str + last_name: Optional[str] = None + middle_name: Optional[str] = None + address: str + zipcode: Optional[str] = None + phone: str + email: Optional[EmailStr] = None + + class Config: + orm_mode = True + from_attributes = True + + +class RecipientCreate(RecipientEdit): + user_id: int + first_name: str + last_name: Optional[str] = None + middle_name: Optional[str] = None + address: str + zipcode: Optional[str] = None + phone: str + email: Optional[EmailStr] = None + + class Config: + orm_mode = True + from_attributes = True + + +class RecipientModel(RecipientCreate): + id: int + + class Config: + orm_mode = True + from_attributes = True diff --git a/backend/tests/test_03_goods.py b/backend/tests/test_03_goods.py index 42eab74..1df4f4a 100644 --- a/backend/tests/test_03_goods.py +++ b/backend/tests/test_03_goods.py @@ -1,5 +1,4 @@ import pytest -import os from constants import GOOD_PAGE_SIZE as PAGE_SIZE token = None @@ -154,8 +153,3 @@ async def test_good_pages(client): response = client.get("/goods?page=-1") assert response.status_code == 422 - # delete token file - os.remove("tests/token.txt") - # delete db file - os.remove("test_database.db") - diff --git a/backend/tests/test_04_payments.py b/backend/tests/test_04_payments.py new file mode 100644 index 0000000..afbb6b7 --- /dev/null +++ b/backend/tests/test_04_payments.py @@ -0,0 +1,49 @@ +import pytest + +token = None + +@pytest.mark.asyncio +async def test_payments(client): + global token + with open("tests/token.txt", "r") as file: + token = file.read() + + response = client.post( + "/payments", + json={"title": "Test Pay", "description": "Test Description"}, + headers={"Authorization": f"Bearer {token}"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["title"] == "Test Pay" + assert data["description"] == "Test Description" + +@pytest.mark.asyncio +async def test_get_payment_by_id(client): + response = client.get("/payments/1") + assert response.status_code == 200 + data = response.json() + assert data["title"] == "Test Pay" + assert data["description"] == "Test Description" + +@pytest.mark.asyncio +async def test_edit_payment_by_id(client): + response = client.patch( + "/payments/1", + json={"title": "Updated Pay", "description": "Updated Description"}, + headers={"Authorization": f"Bearer {token}"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["title"] == "Updated Pay" + assert data["description"] == "Updated Description" + +@pytest.mark.asyncio +async def test_delete_payment_by_id(client): + response = client.delete( + "/payments/1", + headers={"Authorization": f"Bearer {token}"}, + ) + assert response.status_code == 200 + response = client.get("/payments/1") + assert response.status_code == 404 \ No newline at end of file diff --git a/backend/tests/test_05_recipients.py b/backend/tests/test_05_recipients.py new file mode 100644 index 0000000..6730e0a --- /dev/null +++ b/backend/tests/test_05_recipients.py @@ -0,0 +1,120 @@ +import os + +import pytest + +token = None + +@pytest.mark.asyncio +async def test_recipients(client): + global token + with open("tests/token.txt", "r") as file: + token = file.read() + + response = client.get( + "/recipients", + headers={"Authorization": f"Bearer {token}"}, + ) + assert response.status_code == 200 + data = response.json() + assert isinstance(data["items"], list) + +@pytest.mark.asyncio +async def test_add_recipient(client): + new_recipient = { + "first_name": "John", + "last_name": "Doe", + "middle_name": "Smith", + "address": "123 Main St", + "zipcode": "12345", + "phone": "555-555-5555", + "email": "john.doe@example.com", + "user_id": 0, + } + + response = client.post( + "/recipients", + json=new_recipient, + headers={"Authorization": f"Bearer {token}"}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["first_name"] == new_recipient["first_name"] + assert data["last_name"] == new_recipient["last_name"] + assert data["email"] == new_recipient["email"] + +@pytest.mark.asyncio +async def test_get_recipient_by_id(client): + recipient_id = 1 + recipient = { + "first_name": "John", + "last_name": "Doe", + "middle_name": "Smith", + "address": "123 Main St", + "zipcode": "12345", + "phone": "555-555-5555", + "email": "john.doe@example.com", + } + response = client.get( + f"/recipients/{recipient_id}", + headers={"Authorization": f"Bearer {token}"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["id"] == recipient_id + assert data["first_name"] == recipient["first_name"] + assert data["last_name"] == recipient["last_name"] + assert data["email"] == recipient["email"] + assert data["address"] == recipient["address"] + assert data["zipcode"] == recipient["zipcode"] + assert data["phone"] == recipient["phone"] + assert data["email"] == recipient["email"] + +@pytest.mark.asyncio +async def test_update_recipient(client): + recipient_id = 1 + updated_recipient = { + "first_name": "John", + "last_name": "Doe", + "middle_name": "Smith", + "address": "123 Main St", + "zipcode": "12345", + "phone": "666-666-6666", + "email": "john.doe@example.ru", + } + response = client.patch( + f"/recipients/{recipient_id}", + json=updated_recipient, + headers={"Authorization": f"Bearer {token}"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["first_name"] == updated_recipient["first_name"] + assert data["last_name"] == updated_recipient["last_name"] + assert data["email"] == updated_recipient["email"] + assert data["address"] == updated_recipient["address"] + assert data["zipcode"] == updated_recipient["zipcode"] + assert data["phone"] == updated_recipient["phone"] + assert data["email"] == updated_recipient["email"] + assert data["id"] == recipient_id + +@pytest.mark.asyncio +async def test_delete_recipient(client): + recipient_id = 1 + response = client.delete( + f"/recipients/{recipient_id}", + headers={"Authorization": f"Bearer {token}"}, + ) + assert response.status_code == 200 + + # Проверяем, что получатель был удален + response = client.get( + f"/recipients/{recipient_id}", + headers={"Authorization": f"Bearer {token}"}, + ) + assert response.status_code == 404 + + # delete token file + os.remove("tests/token.txt") + # delete db file + os.remove("test_database.db") \ No newline at end of file diff --git a/backend/utils/auth.py b/backend/utils/auth.py index ebe78e9..1cae77d 100644 --- a/backend/utils/auth.py +++ b/backend/utils/auth.py @@ -1,13 +1,12 @@ from datetime import datetime, timedelta +from email.mime.text import MIMEText from random import randint from smtplib import SMTP -from email.mime.text import MIMEText - -from fastapi import Depends, HTTPException -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -from constants import SENDER_EMAIL, SENDER_PASSWORD, SECRET_KEY, ALGORITHM import jwt +from constants import ALGORITHM, SECRET_KEY, SENDER_EMAIL, SENDER_PASSWORD +from fastapi import Depends, HTTPException +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer security = HTTPBearer() @@ -64,3 +63,4 @@ async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(secur raise HTTPException(status_code=401, detail="Token has expired") except jwt.InvalidTokenError: raise HTTPException(status_code=401, detail="Invalid token") + diff --git a/backend/validators.py b/backend/validators.py index a3d3b50..5a59791 100644 --- a/backend/validators.py +++ b/backend/validators.py @@ -43,4 +43,3 @@ async def validate_category_exists(category_id: int, db: AsyncSession = Depends( category = result.scalar_one_or_none() if not category: raise HTTPException(status_code=404, detail="Category not found") -