Files
filam3d/backend/app/routers/clients.py
2026-03-22 14:26:45 +03:00

182 lines
6.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logging
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel
from sqlalchemy import select, desc
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models.client import Client
from app.models.order import Order
from app.models.calculation import Calculation
from app.models.material import Material
from app.services.auth import hash_password, verify_password, create_client_token, decode_access_token
logger = logging.getLogger("app.routers.clients")
router = APIRouter()
bearer = HTTPBearer(auto_error=False)
# ─── Helpers ─────────────────────────────────────────────
async def get_current_client(
creds: HTTPAuthorizationCredentials = Depends(bearer),
db: AsyncSession = Depends(get_db),
) -> Client:
if not creds:
raise HTTPException(401, "Требуется авторизация")
payload = decode_access_token(creds.credentials)
if not payload or payload.get("type") != "client":
raise HTTPException(401, "Невалидный токен")
result = await db.execute(select(Client).where(Client.id == int(payload["sub"])))
client = result.scalar_one_or_none()
if not client or not client.is_active:
raise HTTPException(401, "Клиент не найден или деактивирован")
return client
# ─── Auth ────────────────────────────────────────────────
class RegisterRequest(BaseModel):
email: str
password: str
name: str
phone: str | None = None
company: str | None = None
class LoginRequest(BaseModel):
email: str
password: str
class AuthResponse(BaseModel):
token: str
client: dict
@router.post("/register", response_model=AuthResponse)
async def register(data: RegisterRequest, db: AsyncSession = Depends(get_db)):
logger.info("Client register: %s", data.email)
existing = await db.execute(select(Client).where(Client.email == data.email))
if existing.scalar_one_or_none():
raise HTTPException(400, "Пользователь с таким email уже зарегистрирован")
client = Client(
email=data.email,
password_hash=hash_password(data.password),
name=data.name,
phone=data.phone,
company=data.company,
)
db.add(client)
await db.commit()
await db.refresh(client)
logger.info("Client registered: id=%d, email=%s", client.id, client.email)
token = create_client_token(client.id, client.email)
return AuthResponse(
token=token,
client={"id": client.id, "email": client.email, "name": client.name, "phone": client.phone, "company": client.company},
)
@router.post("/login", response_model=AuthResponse)
async def login(data: LoginRequest, db: AsyncSession = Depends(get_db)):
logger.info("Client login: %s", data.email)
result = await db.execute(select(Client).where(Client.email == data.email, Client.is_active == True))
client = result.scalar_one_or_none()
if not client or not verify_password(data.password, client.password_hash):
raise HTTPException(401, "Неверный email или пароль")
token = create_client_token(client.id, client.email)
logger.info("Client logged in: id=%d", client.id)
return AuthResponse(
token=token,
client={"id": client.id, "email": client.email, "name": client.name, "phone": client.phone, "company": client.company},
)
# ─── Profile ────────────────────────────────────────────
class ProfileResponse(BaseModel):
id: int
email: str
name: str
phone: str | None
company: str | None
@router.get("/me", response_model=ProfileResponse)
async def get_profile(client: Client = Depends(get_current_client)):
return ProfileResponse(id=client.id, email=client.email, name=client.name, phone=client.phone, company=client.company)
class ProfileUpdate(BaseModel):
name: str | None = None
phone: str | None = None
company: str | None = None
@router.put("/me", response_model=ProfileResponse)
async def update_profile(data: ProfileUpdate, client: Client = Depends(get_current_client), db: AsyncSession = Depends(get_db)):
if data.name is not None:
client.name = data.name
if data.phone is not None:
client.phone = data.phone
if data.company is not None:
client.company = data.company
await db.commit()
await db.refresh(client)
return ProfileResponse(id=client.id, email=client.email, name=client.name, phone=client.phone, company=client.company)
# ─── Client Orders ───────────────────────────────────────
class ClientOrderOut(BaseModel):
order_id: str
status: str
total_rub: float
material_name: str | None = None
file_name: str | None = None
quantity: int | None = None
created_at: str
delivery_method: str
comment: str | None = None
@router.get("/orders", response_model=list[ClientOrderOut])
async def get_my_orders(client: Client = Depends(get_current_client), db: AsyncSession = Depends(get_db)):
logger.info("Client orders requested: client_id=%d", client.id)
result = await db.execute(
select(Order).where(Order.client_id == client.id).order_by(desc(Order.created_at))
)
orders = result.scalars().all()
out = []
for o in orders:
calc_result = await db.execute(select(Calculation).where(Calculation.id == o.calculation_id))
calc = calc_result.scalar_one_or_none()
material_name = None
if calc:
mat_result = await db.execute(select(Material).where(Material.id == calc.material_id))
mat = mat_result.scalar_one_or_none()
material_name = mat.name if mat else None
out.append(ClientOrderOut(
order_id=o.order_id,
status=o.status,
total_rub=o.total_rub,
material_name=material_name,
file_name=calc.file_name if calc else None,
quantity=calc.quantity if calc else None,
created_at=o.created_at.isoformat() if o.created_at else "",
delivery_method=o.delivery_method,
comment=o.comment,
))
logger.info("Returning %d orders for client_id=%d", len(out), client.id)
return out