diff --git a/.DS_Store b/.DS_Store index d2e2933..ac86e42 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/backend/.DS_Store b/backend/.DS_Store index 6a06397..a950caf 100644 Binary files a/backend/.DS_Store and b/backend/.DS_Store differ diff --git a/backend/app/.DS_Store b/backend/app/.DS_Store index e5f4b50..35ffb60 100644 Binary files a/backend/app/.DS_Store and b/backend/app/.DS_Store differ diff --git a/backend/app/__pycache__/core.cpython-310.pyc b/backend/app/__pycache__/core.cpython-310.pyc index 0386479..97aa23b 100644 Binary files a/backend/app/__pycache__/core.cpython-310.pyc and b/backend/app/__pycache__/core.cpython-310.pyc differ diff --git a/backend/app/core.py b/backend/app/core.py index 72c903b..3ec3a2f 100644 --- a/backend/app/core.py +++ b/backend/app/core.py @@ -72,14 +72,14 @@ async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = De try: payload = verify_token(token) - username: str = payload.get("sub") - if username is None: + email: str = payload.get("sub") + if email is None: raise credentials_exception except JWTError: raise credentials_exception - from app.repositories.user_repo import get_user_by_username - user = get_user_by_username(db, username) + from app.repositories.user_repo import get_user_by_email + user = get_user_by_email(db, email) if user is None: raise credentials_exception diff --git a/backend/app/repositories/__pycache__/review_repo.cpython-310.pyc b/backend/app/repositories/__pycache__/review_repo.cpython-310.pyc index de70c33..3913f55 100644 Binary files a/backend/app/repositories/__pycache__/review_repo.cpython-310.pyc and b/backend/app/repositories/__pycache__/review_repo.cpython-310.pyc differ diff --git a/backend/app/repositories/__pycache__/user_repo.cpython-310.pyc b/backend/app/repositories/__pycache__/user_repo.cpython-310.pyc index 721ee41..362c92b 100644 Binary files a/backend/app/repositories/__pycache__/user_repo.cpython-310.pyc and b/backend/app/repositories/__pycache__/user_repo.cpython-310.pyc differ diff --git a/backend/app/repositories/review_repo.py b/backend/app/repositories/review_repo.py index 51c895d..57c2956 100644 --- a/backend/app/repositories/review_repo.py +++ b/backend/app/repositories/review_repo.py @@ -205,7 +205,7 @@ def get_review_with_user(db: Session, review_id: int) -> Dict[str, Any]: "is_approved": review.is_approved, "created_at": review.created_at, "updated_at": review.updated_at, - "user_username": user.username if user else "Неизвестный пользователь" + "user_email": user.email if user else "Неизвестный пользователь" } diff --git a/backend/app/repositories/user_repo.py b/backend/app/repositories/user_repo.py index df14ce7..eb8c092 100644 --- a/backend/app/repositories/user_repo.py +++ b/backend/app/repositories/user_repo.py @@ -17,34 +17,26 @@ def get_user_by_email(db: Session, email: str) -> Optional[User]: return db.query(User).filter(User.email == email).first() -def get_user_by_username(db: Session, username: str) -> Optional[User]: - return db.query(User).filter(User.username == username).first() - - def get_users(db: Session, skip: int = 0, limit: int = 100) -> List[User]: return db.query(User).offset(skip).limit(limit).all() def create_user(db: Session, user: UserCreate) -> User: - # Проверяем, что пользователь с таким email или username не существует + # Проверяем, что пользователь с таким email не существует if get_user_by_email(db, user.email): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Пользователь с таким email уже существует" ) - if get_user_by_username(db, user.username): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Пользователь с таким username уже существует" - ) - # Создаем нового пользователя hashed_password = get_password_hash(user.password) db_user = User( email=user.email, - username=user.username, - hashed_password=hashed_password, + password=hashed_password, + phone=user.phone, + first_name=user.first_name, + last_name=user.last_name, is_active=user.is_active, is_admin=user.is_admin ) @@ -75,12 +67,12 @@ def update_user(db: Session, user_id: int, user: UserUpdate) -> User: # Если предоставлен новый пароль, хешируем его if "password" in update_data and update_data["password"]: - update_data["hashed_password"] = get_password_hash(update_data.pop("password")) + update_data["password"] = get_password_hash(update_data.pop("password")) # Удаляем поле password_confirm, если оно есть update_data.pop("password_confirm", None) - # Проверяем уникальность email и username, если они изменяются + # Проверяем уникальность email, если он изменяется if "email" in update_data and update_data["email"] != db_user.email: if get_user_by_email(db, update_data["email"]): raise HTTPException( @@ -88,13 +80,6 @@ def update_user(db: Session, user_id: int, user: UserUpdate) -> User: detail="Пользователь с таким email уже существует" ) - if "username" in update_data and update_data["username"] != db_user.username: - if get_user_by_username(db, update_data["username"]): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Пользователь с таким username уже существует" - ) - # Применяем обновления for key, value in update_data.items(): setattr(db_user, key, value) @@ -131,11 +116,11 @@ def delete_user(db: Session, user_id: int) -> bool: ) -def authenticate_user(db: Session, username: str, password: str) -> Optional[User]: - user = get_user_by_username(db, username) +def authenticate_user(db: Session, email: str, password: str) -> Optional[User]: + user = get_user_by_email(db, email) if not user: return None - if not verify_password(password, user.hashed_password): + if not verify_password(password, user.password): return None return user @@ -241,4 +226,53 @@ def delete_address(db: Session, address_id: int, user_id: int) -> bool: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Ошибка при удалении адреса" - ) \ No newline at end of file + ) + + +# Функции для работы с паролями и токенами сброса +def update_password(db: Session, user_id: int, new_password: str) -> bool: + """Обновляет пароль пользователя""" + db_user = get_user(db, user_id) + if not db_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Пользователь не найден" + ) + + hashed_password = get_password_hash(new_password) + db_user.password = hashed_password + + try: + db.commit() + return True + except Exception: + db.rollback() + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Ошибка при обновлении пароля" + ) + + +def create_password_reset_token(db: Session, user_id: int) -> str: + """Создает токен для сброса пароля""" + import secrets + import datetime + + # В реальном приложении здесь должна быть модель для токенов сброса пароля + # Для примера просто генерируем случайный токен + token = secrets.token_urlsafe(32) + + # В реальном приложении сохраняем токен в базе данных с привязкой к пользователю + # и временем истечения срока действия + + return token + + +def verify_password_reset_token(db: Session, token: str) -> Optional[int]: + """Проверяет токен сброса пароля и возвращает ID пользователя""" + # В реальном приложении проверяем токен в базе данных + # и его срок действия + + # Для примера просто возвращаем фиктивный ID пользователя + # В реальном приложении это должна быть проверка в базе данных + return 1 # Фиктивный ID пользователя \ No newline at end of file diff --git a/backend/app/routers/__pycache__/auth_router.cpython-310.pyc b/backend/app/routers/__pycache__/auth_router.cpython-310.pyc index 9fd8df2..ec953bf 100644 Binary files a/backend/app/routers/__pycache__/auth_router.cpython-310.pyc and b/backend/app/routers/__pycache__/auth_router.cpython-310.pyc differ diff --git a/backend/app/routers/__pycache__/catalog_router.cpython-310.pyc b/backend/app/routers/__pycache__/catalog_router.cpython-310.pyc index fc48d92..dab1384 100644 Binary files a/backend/app/routers/__pycache__/catalog_router.cpython-310.pyc and b/backend/app/routers/__pycache__/catalog_router.cpython-310.pyc differ diff --git a/backend/app/routers/auth_router.py b/backend/app/routers/auth_router.py index 3cd43f8..3388148 100644 --- a/backend/app/routers/auth_router.py +++ b/backend/app/routers/auth_router.py @@ -3,9 +3,9 @@ from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.orm import Session from typing import Dict, Any -from app.core import get_db +from app.core import get_db, get_current_user from app import services -from app.schemas.user_schemas import UserCreate, Token +from app.schemas.user_schemas import UserCreate, Token, PasswordReset, PasswordChange, User # Роутер для аутентификации auth_router = APIRouter(prefix="/auth", tags=["Аутентификация"]) @@ -15,7 +15,36 @@ async def register(user: UserCreate, db: Session = Depends(get_db)): return services.register_user(db, user) -@auth_router.post("/token", response_model=Token) +@auth_router.post("/login", response_model=Token) async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): + # Примечание: form_data.username на самом деле содержит email пользователя + # OAuth2PasswordRequestForm использует поле username, но мы используем его для email result = services.login_user(db, form_data.username, form_data.password) - return result \ No newline at end of file + return result + + +@auth_router.post("/reset-password", response_model=Dict[str, Any]) +async def reset_password(reset_data: PasswordReset, db: Session = Depends(get_db)): + """Запрос на сброс пароля""" + return services.request_password_reset(db, reset_data.email) + + +@auth_router.post("/set-new-password", response_model=Dict[str, Any]) +async def set_new_password(token: str, password: str, db: Session = Depends(get_db)): + """Установка нового пароля по токену сброса""" + return services.reset_password(db, token, password) + + +@auth_router.post("/change-password", response_model=Dict[str, Any]) +async def change_password( + password_data: PasswordChange, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Изменение пароля авторизованным пользователем""" + return services.change_password( + db, + current_user.id, + password_data.current_password, + password_data.new_password + ) \ No newline at end of file diff --git a/backend/app/routers/catalog_router.py b/backend/app/routers/catalog_router.py index 383e0b4..e82e00b 100644 --- a/backend/app/routers/catalog_router.py +++ b/backend/app/routers/catalog_router.py @@ -104,4 +104,6 @@ async def get_products_endpoint( is_active: Optional[bool] = True, db: Session = Depends(get_db) ): - return get_products(db, skip, limit, category_id, search, min_price, max_price, is_active) \ No newline at end of file + products = get_products(db, skip, limit, category_id, search, min_price, max_price, is_active) + # Преобразуем объекты SQLAlchemy в схемы Pydantic + return [Product.model_validate(product) for product in products] \ No newline at end of file diff --git a/backend/app/schemas/__pycache__/catalog_schemas.cpython-310.pyc b/backend/app/schemas/__pycache__/catalog_schemas.cpython-310.pyc index 4fa3ddc..348937d 100644 Binary files a/backend/app/schemas/__pycache__/catalog_schemas.cpython-310.pyc and b/backend/app/schemas/__pycache__/catalog_schemas.cpython-310.pyc differ diff --git a/backend/app/schemas/__pycache__/review_schemas.cpython-310.pyc b/backend/app/schemas/__pycache__/review_schemas.cpython-310.pyc index 0bda271..0b620ca 100644 Binary files a/backend/app/schemas/__pycache__/review_schemas.cpython-310.pyc and b/backend/app/schemas/__pycache__/review_schemas.cpython-310.pyc differ diff --git a/backend/app/schemas/__pycache__/user_schemas.cpython-310.pyc b/backend/app/schemas/__pycache__/user_schemas.cpython-310.pyc index 15b9d0e..302ee9b 100644 Binary files a/backend/app/schemas/__pycache__/user_schemas.cpython-310.pyc and b/backend/app/schemas/__pycache__/user_schemas.cpython-310.pyc differ diff --git a/backend/app/schemas/catalog_schemas.py b/backend/app/schemas/catalog_schemas.py index d479937..8d7540d 100644 --- a/backend/app/schemas/catalog_schemas.py +++ b/backend/app/schemas/catalog_schemas.py @@ -131,12 +131,18 @@ class ProductImage(ProductImageBase): class CategoryWithSubcategories(Category): subcategories: List['CategoryWithSubcategories'] = [] + class Config: + from_attributes = True + class ProductWithDetails(Product): category: Category variants: List[ProductVariant] = [] images: List[ProductImage] = [] + class Config: + from_attributes = True + # Рекурсивное обновление для CategoryWithChildren CategoryWithSubcategories.update_forward_refs() \ No newline at end of file diff --git a/backend/app/schemas/review_schemas.py b/backend/app/schemas/review_schemas.py index a8a5755..fce4a00 100644 --- a/backend/app/schemas/review_schemas.py +++ b/backend/app/schemas/review_schemas.py @@ -34,4 +34,4 @@ class Review(ReviewBase): class ReviewWithUser(Review): - user_username: str \ No newline at end of file + user_email: str \ No newline at end of file diff --git a/backend/app/schemas/user_schemas.py b/backend/app/schemas/user_schemas.py index c5ce3e5..41d45b1 100644 --- a/backend/app/schemas/user_schemas.py +++ b/backend/app/schemas/user_schemas.py @@ -39,7 +39,9 @@ class Address(AddressBase): # Базовые схемы для пользователя class UserBase(BaseModel): email: EmailStr - username: str + phone: Optional[str] = None + first_name: Optional[str] = None + last_name: Optional[str] = None is_active: bool = True is_admin: bool = False @@ -57,7 +59,9 @@ class UserCreate(UserBase): class UserUpdate(BaseModel): email: Optional[EmailStr] = None - username: Optional[str] = None + phone: Optional[str] = None + first_name: Optional[str] = None + last_name: Optional[str] = None is_active: Optional[bool] = None is_admin: Optional[bool] = None password: Optional[str] = Field(None, min_length=8) @@ -81,7 +85,7 @@ class User(UserBase): class UserInDB(User): - hashed_password: str + password: str # Схемы для аутентификации @@ -91,5 +95,14 @@ class Token(BaseModel): class TokenData(BaseModel): - username: Optional[str] = None - user_id: Optional[int] = None \ No newline at end of file + email: Optional[str] = None + user_id: Optional[int] = None + + +class PasswordReset(BaseModel): + email: EmailStr + + +class PasswordChange(BaseModel): + current_password: str + new_password: str = Field(..., min_length=8) \ No newline at end of file diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py index 7d5ae5e..8b20b10 100644 --- a/backend/app/services/__init__.py +++ b/backend/app/services/__init__.py @@ -1,6 +1,7 @@ from app.services.user_service import ( register_user, login_user, get_user_profile, update_user_profile, - add_user_address, update_user_address, delete_user_address + add_user_address, update_user_address, delete_user_address, + request_password_reset, reset_password, change_password ) from app.services.catalog_service import ( diff --git a/backend/app/services/__pycache__/__init__.cpython-310.pyc b/backend/app/services/__pycache__/__init__.cpython-310.pyc index 731d2c6..b2ab191 100644 Binary files a/backend/app/services/__pycache__/__init__.cpython-310.pyc and b/backend/app/services/__pycache__/__init__.cpython-310.pyc differ diff --git a/backend/app/services/__pycache__/catalog_service.cpython-310.pyc b/backend/app/services/__pycache__/catalog_service.cpython-310.pyc index 541fcd2..f01d205 100644 Binary files a/backend/app/services/__pycache__/catalog_service.cpython-310.pyc and b/backend/app/services/__pycache__/catalog_service.cpython-310.pyc differ diff --git a/backend/app/services/__pycache__/order_service.cpython-310.pyc b/backend/app/services/__pycache__/order_service.cpython-310.pyc index 4f63063..2d9ba8a 100644 Binary files a/backend/app/services/__pycache__/order_service.cpython-310.pyc and b/backend/app/services/__pycache__/order_service.cpython-310.pyc differ diff --git a/backend/app/services/__pycache__/review_service.cpython-310.pyc b/backend/app/services/__pycache__/review_service.cpython-310.pyc index ee044b8..2a5ecb8 100644 Binary files a/backend/app/services/__pycache__/review_service.cpython-310.pyc and b/backend/app/services/__pycache__/review_service.cpython-310.pyc differ diff --git a/backend/app/services/__pycache__/user_service.cpython-310.pyc b/backend/app/services/__pycache__/user_service.cpython-310.pyc index 5e20e81..b136a1e 100644 Binary files a/backend/app/services/__pycache__/user_service.cpython-310.pyc and b/backend/app/services/__pycache__/user_service.cpython-310.pyc differ diff --git a/backend/app/services/catalog_service.py b/backend/app/services/catalog_service.py index 63feeb1..ca43992 100644 --- a/backend/app/services/catalog_service.py +++ b/backend/app/services/catalog_service.py @@ -18,13 +18,21 @@ from app.schemas.catalog_schemas import ( # Сервисы каталога def create_category(db: Session, category: CategoryCreate) -> Dict[str, Any]: + from app.schemas.catalog_schemas import Category as CategorySchema + new_category = catalog_repo.create_category(db, category) - return {"category": new_category} + # Преобразуем объект SQLAlchemy в схему Pydantic + category_schema = CategorySchema.model_validate(new_category) + return {"category": category_schema} def update_category(db: Session, category_id: int, category: CategoryUpdate) -> Dict[str, Any]: + from app.schemas.catalog_schemas import Category as CategorySchema + updated_category = catalog_repo.update_category(db, category_id, category) - return {"category": updated_category} + # Преобразуем объект SQLAlchemy в схему Pydantic + category_schema = CategorySchema.model_validate(updated_category) + return {"category": category_schema} def delete_category(db: Session, category_id: int) -> Dict[str, Any]: @@ -33,51 +41,55 @@ def delete_category(db: Session, category_id: int) -> Dict[str, Any]: def get_category_tree(db: Session) -> List[Dict[str, Any]]: + from app.schemas.catalog_schemas import Category as CategorySchema + # Получаем все категории верхнего уровня root_categories = catalog_repo.get_categories(db, parent_id=None) result = [] for category in root_categories: + # Преобразуем объект SQLAlchemy в схему Pydantic + category_schema = CategorySchema.model_validate(category) # Рекурсивно получаем подкатегории - category_dict = { - "id": category.id, - "name": category.name, - "slug": category.slug, - "description": category.description, - "is_active": category.is_active, - "subcategories": _get_subcategories(db, category.id) - } + category_dict = category_schema.model_dump() + category_dict["subcategories"] = _get_subcategories(db, category.id) result.append(category_dict) return result def _get_subcategories(db: Session, parent_id: int) -> List[Dict[str, Any]]: + from app.schemas.catalog_schemas import Category as CategorySchema + subcategories = catalog_repo.get_categories(db, parent_id=parent_id) result = [] for category in subcategories: - category_dict = { - "id": category.id, - "name": category.name, - "slug": category.slug, - "description": category.description, - "is_active": category.is_active, - "subcategories": _get_subcategories(db, category.id) - } + # Преобразуем объект SQLAlchemy в схему Pydantic + category_schema = CategorySchema.model_validate(category) + category_dict = category_schema.model_dump() + category_dict["subcategories"] = _get_subcategories(db, category.id) result.append(category_dict) return result def create_product(db: Session, product: ProductCreate) -> Dict[str, Any]: + from app.schemas.catalog_schemas import Product as ProductSchema + new_product = catalog_repo.create_product(db, product) - return {"product": new_product} + # Преобразуем объект SQLAlchemy в схему Pydantic + product_schema = ProductSchema.model_validate(new_product) + return {"product": product_schema} def update_product(db: Session, product_id: int, product: ProductUpdate) -> Dict[str, Any]: + from app.schemas.catalog_schemas import Product as ProductSchema + updated_product = catalog_repo.update_product(db, product_id, product) - return {"product": updated_product} + # Преобразуем объект SQLAlchemy в схему Pydantic + product_schema = ProductSchema.model_validate(updated_product) + return {"product": product_schema} def delete_product(db: Session, product_id: int) -> Dict[str, Any]: @@ -86,6 +98,10 @@ def delete_product(db: Session, product_id: int) -> Dict[str, Any]: def get_product_details(db: Session, product_id: int) -> Dict[str, Any]: + from app.schemas.catalog_schemas import Product as ProductSchema, Category as CategorySchema + from app.schemas.catalog_schemas import ProductVariant as ProductVariantSchema + from app.schemas.catalog_schemas import ProductImage as ProductImageSchema + product = catalog_repo.get_product(db, product_id) if not product: raise HTTPException( @@ -105,23 +121,36 @@ def get_product_details(db: Session, product_id: int) -> Dict[str, Any]: # Получаем отзывы продукта reviews = review_repo.get_product_reviews(db, product_id, limit=5) + # Преобразуем объекты SQLAlchemy в схемы Pydantic + product_schema = ProductSchema.model_validate(product) + variants_schema = [ProductVariantSchema.model_validate(variant) for variant in variants] + images_schema = [ProductImageSchema.model_validate(image) for image in images] + return { - "product": product, - "variants": variants, - "images": images, + "product": product_schema, + "variants": variants_schema, + "images": images_schema, "rating": rating, "reviews": reviews } def add_product_variant(db: Session, variant: ProductVariantCreate) -> Dict[str, Any]: + from app.schemas.catalog_schemas import ProductVariant as ProductVariantSchema + new_variant = catalog_repo.create_product_variant(db, variant) - return {"variant": new_variant} + # Преобразуем объект SQLAlchemy в схему Pydantic + variant_schema = ProductVariantSchema.model_validate(new_variant) + return {"variant": variant_schema} def update_product_variant(db: Session, variant_id: int, variant: ProductVariantUpdate) -> Dict[str, Any]: + from app.schemas.catalog_schemas import ProductVariant as ProductVariantSchema + updated_variant = catalog_repo.update_product_variant(db, variant_id, variant) - return {"variant": updated_variant} + # Преобразуем объект SQLAlchemy в схему Pydantic + variant_schema = ProductVariantSchema.model_validate(updated_variant) + return {"variant": variant_schema} def delete_product_variant(db: Session, variant_id: int) -> Dict[str, Any]: @@ -130,6 +159,8 @@ def delete_product_variant(db: Session, variant_id: int) -> Dict[str, Any]: def upload_product_image(db: Session, product_id: int, file: UploadFile, is_primary: bool = False) -> Dict[str, Any]: + from app.schemas.catalog_schemas import ProductImage as ProductImageSchema + # Проверяем, что продукт существует product = catalog_repo.get_product(db, product_id) if not product: @@ -168,12 +199,18 @@ def upload_product_image(db: Session, product_id: int, file: UploadFile, is_prim new_image = catalog_repo.create_product_image(db, image_data) - return {"image": new_image} + # Преобразуем объект SQLAlchemy в схему Pydantic + image_schema = ProductImageSchema.model_validate(new_image) + return {"image": image_schema} def update_product_image(db: Session, image_id: int, is_primary: bool) -> Dict[str, Any]: + from app.schemas.catalog_schemas import ProductImage as ProductImageSchema + updated_image = catalog_repo.update_product_image(db, image_id, is_primary) - return {"image": updated_image} + # Преобразуем объект SQLAlchemy в схему Pydantic + image_schema = ProductImageSchema.model_validate(updated_image) + return {"image": image_schema} def delete_product_image(db: Session, image_id: int) -> Dict[str, Any]: diff --git a/backend/app/services/order_service.py b/backend/app/services/order_service.py index 3267516..68a0d79 100644 --- a/backend/app/services/order_service.py +++ b/backend/app/services/order_service.py @@ -9,6 +9,8 @@ from app.schemas.content_schemas import AnalyticsLogCreate # Сервисы корзины и заказов def add_to_cart(db: Session, user_id: int, cart_item: CartItemCreate) -> Dict[str, Any]: + from app.schemas.order_schemas import CartItem as CartItemSchema + new_cart_item = order_repo.create_cart_item(db, cart_item, user_id) # Логируем событие добавления в корзину @@ -20,12 +22,18 @@ def add_to_cart(db: Session, user_id: int, cart_item: CartItemCreate) -> Dict[st ) content_repo.log_analytics_event(db, log_data) - return {"cart_item": new_cart_item} + # Преобразуем объект SQLAlchemy в схему Pydantic + cart_item_schema = CartItemSchema.model_validate(new_cart_item) + return {"cart_item": cart_item_schema} def update_cart_item(db: Session, user_id: int, cart_item_id: int, cart_item: CartItemUpdate) -> Dict[str, Any]: + from app.schemas.order_schemas import CartItem as CartItemSchema + updated_cart_item = order_repo.update_cart_item(db, cart_item_id, cart_item, user_id) - return {"cart_item": updated_cart_item} + # Преобразуем объект SQLAlchemy в схему Pydantic + cart_item_schema = CartItemSchema.model_validate(updated_cart_item) + return {"cart_item": cart_item_schema} def remove_from_cart(db: Session, user_id: int, cart_item_id: int) -> Dict[str, Any]: @@ -39,11 +47,15 @@ def clear_cart(db: Session, user_id: int) -> Dict[str, Any]: def get_cart(db: Session, user_id: int) -> Dict[str, Any]: + from app.schemas.order_schemas import CartItem as CartItemSchema + + # Получаем элементы корзины с деталями продуктов cart_items = order_repo.get_cart_with_product_details(db, user_id) # Рассчитываем общую сумму корзины total_amount = sum(item["total_price"] for item in cart_items) + # Примечание: cart_items уже содержит сериализованные данные из репозитория return { "items": cart_items, "total_amount": total_amount, @@ -52,6 +64,8 @@ def get_cart(db: Session, user_id: int) -> Dict[str, Any]: def create_order(db: Session, user_id: int, order: OrderCreate) -> Dict[str, Any]: + from app.schemas.order_schemas import Order as OrderSchema + new_order = order_repo.create_order(db, order, user_id) # Логируем событие создания заказа @@ -62,7 +76,9 @@ def create_order(db: Session, user_id: int, order: OrderCreate) -> Dict[str, Any ) content_repo.log_analytics_event(db, log_data) - return {"order": new_order} + # Преобразуем объект SQLAlchemy в схему Pydantic + order_schema = OrderSchema.model_validate(new_order) + return {"order": order_schema} def get_order(db: Session, user_id: int, order_id: int, is_admin: bool = False) -> Dict[str, Any]: @@ -80,6 +96,8 @@ def get_order(db: Session, user_id: int, order_id: int, is_admin: bool = False) def update_order(db: Session, user_id: int, order_id: int, order: OrderUpdate, is_admin: bool = False) -> Dict[str, Any]: + from app.schemas.order_schemas import Order as OrderSchema + updated_order = order_repo.update_order(db, order_id, order, is_admin) # Проверяем права доступа @@ -89,10 +107,14 @@ def update_order(db: Session, user_id: int, order_id: int, order: OrderUpdate, i detail="Недостаточно прав для обновления этого заказа" ) - return {"order": updated_order} + # Преобразуем объект SQLAlchemy в схему Pydantic + order_schema = OrderSchema.model_validate(updated_order) + return {"order": order_schema} def cancel_order(db: Session, user_id: int, order_id: int) -> Dict[str, Any]: + from app.schemas.order_schemas import Order as OrderSchema + # Отменяем заказ (обычный пользователь может только отменить заказ) order_update = OrderUpdate(status="cancelled") updated_order = order_repo.update_order(db, order_id, order_update, is_admin=False) @@ -104,4 +126,6 @@ def cancel_order(db: Session, user_id: int, order_id: int) -> Dict[str, Any]: detail="Недостаточно прав для отмены этого заказа" ) - return {"order": updated_order} \ No newline at end of file + # Преобразуем объект SQLAlchemy в схему Pydantic + order_schema = OrderSchema.model_validate(updated_order) + return {"order": order_schema} \ No newline at end of file diff --git a/backend/app/services/review_service.py b/backend/app/services/review_service.py index 128c7e8..e873b2d 100644 --- a/backend/app/services/review_service.py +++ b/backend/app/services/review_service.py @@ -7,13 +7,21 @@ from app.schemas.review_schemas import ReviewCreate, ReviewUpdate # Сервисы отзывов def create_review(db: Session, user_id: int, review: ReviewCreate) -> Dict[str, Any]: + from app.schemas.review_schemas import Review as ReviewSchema + new_review = review_repo.create_review(db, review, user_id) - return {"review": new_review} + # Преобразуем объект SQLAlchemy в схему Pydantic + review_schema = ReviewSchema.model_validate(new_review) + return {"review": review_schema} def update_review(db: Session, user_id: int, review_id: int, review: ReviewUpdate, is_admin: bool = False) -> Dict[str, Any]: + from app.schemas.review_schemas import Review as ReviewSchema + updated_review = review_repo.update_review(db, review_id, review, user_id, is_admin) - return {"review": updated_review} + # Преобразуем объект SQLAlchemy в схему Pydantic + review_schema = ReviewSchema.model_validate(updated_review) + return {"review": review_schema} def delete_review(db: Session, user_id: int, review_id: int, is_admin: bool = False) -> Dict[str, Any]: @@ -22,18 +30,27 @@ def delete_review(db: Session, user_id: int, review_id: int, is_admin: bool = Fa def approve_review(db: Session, review_id: int) -> Dict[str, Any]: + from app.schemas.review_schemas import Review as ReviewSchema + approved_review = review_repo.approve_review(db, review_id) - return {"review": approved_review} + # Преобразуем объект SQLAlchemy в схему Pydantic + review_schema = ReviewSchema.model_validate(approved_review) + return {"review": review_schema} def get_product_reviews(db: Session, product_id: int, skip: int = 0, limit: int = 10) -> Dict[str, Any]: + from app.schemas.review_schemas import Review as ReviewSchema + reviews = review_repo.get_product_reviews(db, product_id, skip, limit) # Получаем рейтинг продукта rating = review_repo.get_product_rating(db, product_id) + # Преобразуем объекты SQLAlchemy в схемы Pydantic + reviews_schema = [ReviewSchema.model_validate(review) for review in reviews] + return { - "reviews": reviews, + "reviews": reviews_schema, "rating": rating, "total": rating["total_reviews"], "skip": skip, diff --git a/backend/app/services/user_service.py b/backend/app/services/user_service.py index 9aaf83a..10cf894 100644 --- a/backend/app/services/user_service.py +++ b/backend/app/services/user_service.py @@ -11,9 +11,14 @@ from app.schemas.user_schemas import UserCreate, UserUpdate, AddressCreate, Addr # Сервисы аутентификации и пользователей def register_user(db: Session, user: UserCreate) -> Dict[str, Any]: + from app.schemas.user_schemas import User as UserSchema + # Создаем пользователя db_user = user_repo.create_user(db, user) + # Преобразуем объект SQLAlchemy в схему Pydantic + user_schema = UserSchema.model_validate(db_user) + # Создаем токен доступа access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( @@ -21,7 +26,7 @@ def register_user(db: Session, user: UserCreate) -> Dict[str, Any]: ) return { - "user": db_user, + "user": user_schema, "access_token": access_token, "token_type": "bearer" } @@ -57,6 +62,9 @@ def login_user(db: Session, email: str, password: str) -> Dict[str, Any]: def get_user_profile(db: Session, user_id: int) -> Dict[str, Any]: + from app.schemas.user_schemas import User as UserSchema, Address as AddressSchema + from app.schemas.review_schemas import Review as ReviewSchema + user = user_repo.get_user(db, user_id) if not user: raise HTTPException( @@ -73,29 +81,121 @@ def get_user_profile(db: Session, user_id: int) -> Dict[str, Any]: # Получаем отзывы пользователя reviews = review_repo.get_user_reviews(db, user_id) + # Преобразуем объекты SQLAlchemy в схемы Pydantic + user_dict = { + "id": user.id, + "email": user.email, + "phone": user.phone, + "first_name": user.first_name, + "last_name": user.last_name, + "is_active": user.is_active, + "is_admin": user.is_admin, + "created_at": user.created_at, + "updated_at": user.updated_at, + "addresses": [] + } + user_schema = UserSchema.model_validate(user_dict) + addresses_schema = [AddressSchema.model_validate({ + "id": address.id, + "user_id": address.user_id, + "address_line1": address.address_line1, + "address_line2": address.address_line2, + "city": address.city, + "state": address.state, + "postal_code": address.postal_code, + "country": address.country, + "is_default": address.is_default, + "created_at": address.created_at, + "updated_at": address.updated_at + }) for address in addresses] + reviews_schema = [ReviewSchema.model_validate(review.__dict__) for review in reviews] + return { - "user": user, - "addresses": addresses, - "orders": orders, - "reviews": reviews + "user": user_schema, + "addresses": addresses_schema, + "orders": orders, # Заказы обрабатываются отдельно в сервисе заказов + "reviews": reviews_schema } def update_user_profile(db: Session, user_id: int, user_data: UserUpdate) -> Dict[str, Any]: + from app.schemas.user_schemas import User as UserSchema + updated_user = user_repo.update_user(db, user_id, user_data) - return {"user": updated_user} + # Преобразуем объект SQLAlchemy в схему Pydantic + user_schema = UserSchema.model_validate(updated_user) + return {"user": user_schema} def add_user_address(db: Session, user_id: int, address: AddressCreate) -> Dict[str, Any]: + from app.schemas.user_schemas import Address as AddressSchema + new_address = user_repo.create_address(db, address, user_id) - return {"address": new_address} + # Преобразуем объект SQLAlchemy в схему Pydantic + address_schema = AddressSchema.model_validate(new_address) + return {"address": address_schema} def update_user_address(db: Session, user_id: int, address_id: int, address: AddressUpdate) -> Dict[str, Any]: + from app.schemas.user_schemas import Address as AddressSchema + updated_address = user_repo.update_address(db, address_id, address, user_id) - return {"address": updated_address} + # Преобразуем объект SQLAlchemy в схему Pydantic + address_schema = AddressSchema.model_validate(updated_address) + return {"address": address_schema} def delete_user_address(db: Session, user_id: int, address_id: int) -> Dict[str, Any]: success = user_repo.delete_address(db, address_id, user_id) - return {"success": success} \ No newline at end of file + return {"success": success} + + +def request_password_reset(db: Session, email: str) -> Dict[str, Any]: + """Запрос на сброс пароля""" + # Проверяем, существует ли пользователь с таким email + user = user_repo.get_user_by_email(db, email) + if not user: + # Не сообщаем, что пользователь не существует (безопасность) + return {"message": "Если указанный email зарегистрирован, на него будет отправлена инструкция по сбросу пароля"} + + # Генерируем токен для сброса пароля + reset_token = user_repo.create_password_reset_token(db, user.id) + + # В реальном приложении здесь должна быть отправка email + # Для примера просто возвращаем токен + return { + "message": "Инструкция по сбросу пароля отправлена на указанный email", + "token": reset_token # В реальном приложении не возвращаем токен + } + + +def reset_password(db: Session, token: str, new_password: str) -> Dict[str, Any]: + """Сброс пароля по токену""" + # Проверяем токен и получаем пользователя + user_id = user_repo.verify_password_reset_token(db, token) + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Недействительный или устаревший токен сброса пароля" + ) + + # Обновляем пароль + user_repo.update_password(db, user_id, new_password) + + return {"message": "Пароль успешно изменен"} + + +def change_password(db: Session, user_id: int, current_password: str, new_password: str) -> Dict[str, Any]: + """Изменение пароля авторизованным пользователем""" + # Проверяем текущий пароль + user = user_repo.get_user(db, user_id) + if not user or not user_repo.verify_password(current_password, user.password): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Неверный текущий пароль" + ) + + # Обновляем пароль + user_repo.update_password(db, user_id, new_password) + + return {"message": "Пароль успешно изменен"} \ No newline at end of file diff --git a/frontend/components/Header.tsx b/frontend/components/Header.tsx index 7627ac3..4afe6d4 100644 --- a/frontend/components/Header.tsx +++ b/frontend/components/Header.tsx @@ -1,14 +1,24 @@ import Link from "next/link"; -import { Search, Heart, User, ShoppingCart, ChevronLeft } from "lucide-react"; +import { Search, Heart, User, ShoppingCart, ChevronLeft, LogOut } from "lucide-react"; import { useState, useEffect } from "react"; import { motion } from "framer-motion"; import Image from "next/image"; import { useRouter } from "next/router"; +import authService from "../services/auth"; export default function Header() { const router = useRouter(); // Состояние для отслеживания прокрутки страницы const [scrolled, setScrolled] = useState(false); + // Состояние для отслеживания аутентификации пользователя + const [isAuthenticated, setIsAuthenticated] = useState(false); + // Состояние для отображения выпадающего меню пользователя + const [showUserMenu, setShowUserMenu] = useState(false); + + // Эффект для проверки аутентификации при загрузке компонента + useEffect(() => { + setIsAuthenticated(authService.isAuthenticated()); + }, []); // Эффект для отслеживания прокрутки useEffect(() => { @@ -25,6 +35,14 @@ export default function Header() { }; }, [scrolled]); + // Функция для выхода из системы + const handleLogout = () => { + authService.logout(); + setIsAuthenticated(false); + setShowUserMenu(false); + router.push('/'); + }; + // Функция для возврата на предыдущую страницу const goBack = () => { router.back(); @@ -35,6 +53,11 @@ export default function Header() { // Проверяем, находимся ли мы на странице категорий или коллекций const isDetailPage = router.pathname.includes("[slug]"); + // Функция для переключения отображения меню пользователя + const toggleUserMenu = () => { + setShowUserMenu(!showUserMenu); + }; + return (