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