182 lines
6.6 KiB
Python
182 lines
6.6 KiB
Python
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
|