Skip to content
Merged

Api #12

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from database import create_tables
from fastapi import FastAPI
from routes import good, good_category, login, payment, recipient, basket
from routes import good, good_category, login, payment, recipient, basket, roles


@asynccontextmanager
Expand All @@ -27,6 +27,8 @@ async def lifespan(_: FastAPI):
app.include_router(basket.router, prefix="/api/v1/basket", tags=["Корзина"])
app.include_router(payment.router, prefix="/api/v1/payments", tags=["Методы оплаты"])
app.include_router(recipient.router, prefix="/api/v1/recipients", tags=["Получатели"])

app.include_router(roles.router, prefix="/api/v1/users", tags=["Пользователи и роли"])
app.include_router(login.router, prefix="/api/v1/auth", tags=["Авторизация"])


Expand Down
1 change: 1 addition & 0 deletions backend/models/category.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class GoodCategory(Base):
id = Column(Integer, primary_key=True, index=True)
name = Column(String(255), unique=True, nullable=False)
description = Column(Text, nullable=True)
image_url = Column(String(255), nullable=True)
parent_id = Column(Integer, ForeignKey("good_categories.id", ondelete="SET NULL"), nullable=True)

# Рекурсивная связь для подкатегорий
Expand Down
1 change: 1 addition & 0 deletions backend/models/good.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ class Goods(Base):
name = Column(String(255), nullable=False)
description = Column(Text, nullable=True)
price = Column(Integer, nullable=False)
image_url = Column(String(255), nullable=True)
category_id = Column(Integer, ForeignKey("good_categories.id", ondelete="SET NULL"), nullable=True)

2 changes: 1 addition & 1 deletion backend/models/payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ class PaymentMethods(Base):
id = Column(Integer, primary_key=True, index=True)
title = Column(String(255), nullable=False)
description = Column(Text, nullable=True)
image = Column(String, nullable=True)
image_url = Column(String(255), nullable=True)

21 changes: 18 additions & 3 deletions backend/routes/good_category.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# app/routes/good_category.py
from typing import List

from constants import GOOD_CATEGORY_PAGE_SIZE as PAGE_SIZE, GET_GOOD_CATEGORIES_DESCRIPTION
from database import get_db
from fastapi import APIRouter, Depends, HTTPException, Query, Request

from models.good import Goods
from models.category import GoodCategory
from schemas import GoodCategoryCreate, GoodCategoryModel
from schemas import GoodCategoryCreate, GoodCategoryModel, GoodModel
from responses import GetGoodCategoriesResponse
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
Expand Down Expand Up @@ -40,8 +44,7 @@ async def get_categories(request: Request, page: int = Query(1, ge=1),
}


@router.post("",
response_model=GoodCategoryModel)
@router.post("", response_model=GoodCategoryModel)
async def create_category(category: GoodCategoryCreate, db: AsyncSession = Depends(get_db),
user_data: dict = Depends(verify_token)):
# validation
Expand Down Expand Up @@ -69,6 +72,17 @@ async def get_category(category_id: int, db: AsyncSession = Depends(get_db)):
return category


@router.get("/{category_id}/goods")
async def get_category(category_id: int, db: AsyncSession = Depends(get_db)):
# validation
await validate_category_exists(category_id, db)

# get category
result = await db.execute(select(Goods).filter(Goods.category_id == category_id))
goods = result.scalars().all()
return goods


@router.patch("/{category_id}", response_model=GoodCategoryModel)
async def update_category(category_id: int, category: GoodCategoryCreate, db: AsyncSession = Depends(get_db),
user_data: dict = Depends(verify_token)):
Expand All @@ -86,6 +100,7 @@ async def update_category(category_id: int, category: GoodCategoryCreate, db: As
db_category.name = category.name
db_category.description = category.description
db_category.parent_id = category.parent_id
db_category.image_url = category.image_url
await db.commit()
await db.refresh(db_category) # обновление
return db_category
Expand Down
58 changes: 58 additions & 0 deletions backend/routes/roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# app/routes/roles.py
from models.user import UserRole

from database import get_db, is_testing
from fastapi import APIRouter, Depends, HTTPException, Response, Query
from models.user import OTP, User
from schemas import UserLogin, UserVerify
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from utils.auth import generate_access_token, generate_code, generate_refresh_token, verify_token
from validators import validate_admin_only

router = APIRouter()


@router.get("")
async def get_all_users(user_data: dict = Depends(verify_token), db: AsyncSession = Depends(get_db)):
await validate_admin_only(user_data, db)
result = await db.execute(select(User))
all_users = result.scalars().all()
return all_users


@router.post("/{user_id}/set-role")
async def set_user_role(user_id: int, role: UserRole = Query(UserRole.USER),
user_data: dict = Depends(verify_token), db: AsyncSession = Depends(get_db)):
user = await validate_admin_only(user_data, db)
if role not in [UserRole.USER, UserRole.ADMIN, UserRole.SELLER, UserRole.OWNER]:
raise HTTPException(status_code=403, detail="Invalid role.")
if user_id == user.id:
raise HTTPException(status_code=403, detail="Can't give out a role to yourself")
aim = await db.get(User, user_id)
if not aim:
raise HTTPException(status_code=404, detail="User not found.")
if user.role == role:
raise HTTPException(status_code=403, detail=f"Equal credentials or higher ({role.value})")
if user.role == UserRole.ADMIN and role == UserRole.OWNER:
raise HTTPException(status_code=403, detail=f"Equal credentials or higher ({role.value})")
aim.role = role
await db.commit()
await db.refresh(aim)
return aim


@router.delete("/{user_id}")
async def delete_user(user_id: int, user_data: dict = Depends(verify_token), db: AsyncSession = Depends(get_db)):
user = await validate_admin_only(user_data, db)
if user_id == user.id:
raise HTTPException(status_code=403, detail="Can't delete yourself")
aim = await db.get(User, user_id)
if not aim:
raise HTTPException(status_code=404, detail="User not found.")
if user.role == aim.role:
raise HTTPException(status_code=403, detail=f"Equal credentials or higher ({user.role})")
if user.role == UserRole.ADMIN and aim.role == UserRole.OWNER:
raise HTTPException(status_code=403, detail=f"Equal credentials or higher ({user.role})")
await db.delete(aim)
await db.commit()
4 changes: 3 additions & 1 deletion backend/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class GoodCategoryCreate(BaseModel):
name: str
description: Optional[str] = None # Описание
parent_id: Optional[int] = None # ID родительской категории
image_url: Optional[str] = None

class Config:
orm_mode = True
Expand All @@ -27,6 +28,7 @@ class GoodCreate(BaseModel):
name: str
description: Optional[str] = None # Описание
price: int # Цена
image_url: Optional[str] = None
category_id: Optional[int] = None # ID категории

class Config:
Expand Down Expand Up @@ -65,7 +67,7 @@ class Config:
class PaymentMethodCreate(BaseModel):
title: str
description: Optional[str] = None
image: Optional[str] = None
image_url: Optional[str] = None

class Config:
orm_mode = True
Expand Down
9 changes: 9 additions & 0 deletions backend/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,12 @@ async def validate_staff_only(user_data: dict, db: AsyncSession = Depends(get_db
if db_user.role not in [UserRole.OWNER, UserRole.ADMIN, UserRole.SELLER]:
raise HTTPException(status_code=403, detail="Staff only")
return db_user

async def validate_admin_only(user_data: dict, db: AsyncSession = Depends(get_db)) -> User:
"""Доступ только для администрации"""
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 not in [UserRole.OWNER, UserRole.ADMIN]:
raise HTTPException(status_code=403, detail="Admin only")
return db_user
15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
services:
minio:
image: minio/minio
container_name: minio
environment:
- MINIO_ROOT_USER=${MINIO_ROOT_USER}
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
ports:
- "9000:9000"
volumes:
- minio_data:/data
command: server /data
networks:
- app_network

db:
build:
context: ./db
Expand Down Expand Up @@ -40,6 +54,7 @@ services:

volumes:
postgres_data:
minio_data:

networks:
app_network:
Expand Down
3 changes: 3 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ POSTGRES_DB=db_name
POSTGRES_HOST=host_name
POSTGRES_PORT=5432

MINIO_ROOT_USER=access
MINIO_ROOT_PASSWORD=secret1234

DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
DATABASE_URL_TEST=sqlite+aiosqlite:///./test.db

Expand Down
Loading