CLIO v1.0 — Scanner Inteligente com IA (MVP)
This commit is contained in:
0
backend/app/routers/__init__.py
Normal file
0
backend/app/routers/__init__.py
Normal file
37
backend/app/routers/auth.py
Normal file
37
backend/app/routers/auth.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.schemas.auth import RegisterRequest, LoginRequest, TokenResponse, UserResponse
|
||||
from app.utils.security import hash_password, verify_password, create_access_token, get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
|
||||
def user_to_dict(user: User) -> dict:
|
||||
return {"id": user.id, "email": user.email, "name": user.name, "plan": user.plan}
|
||||
|
||||
@router.post("/register", response_model=TokenResponse)
|
||||
async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)):
|
||||
existing = await db.execute(select(User).where(User.email == req.email))
|
||||
if existing.scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail="Email já cadastrado")
|
||||
user = User(email=req.email, name=req.name or req.email.split("@")[0], password_hash=hash_password(req.password))
|
||||
db.add(user)
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
token = create_access_token({"sub": str(user.id)})
|
||||
return TokenResponse(access_token=token, user=UserResponse(**user_to_dict(user)))
|
||||
|
||||
@router.post("/login", response_model=TokenResponse)
|
||||
async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(User).where(User.email == req.email))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user or not verify_password(req.password, user.password_hash):
|
||||
raise HTTPException(status_code=401, detail="Email ou senha incorretos")
|
||||
token = create_access_token({"sub": str(user.id)})
|
||||
return TokenResponse(access_token=token, user=UserResponse(**user_to_dict(user)))
|
||||
|
||||
@router.get("/me")
|
||||
async def me(user: User = Depends(get_current_user)):
|
||||
return user_to_dict(user)
|
||||
143
backend/app/routers/documents.py
Normal file
143
backend/app/routers/documents.py
Normal file
@@ -0,0 +1,143 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, desc
|
||||
from typing import Optional
|
||||
import base64
|
||||
import os
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.models.document import Document
|
||||
from app.schemas.document import ScanRequest, DocumentResponse, DocumentListResponse
|
||||
from app.services.ai_service import analyze_document
|
||||
from app.utils.security import get_current_user
|
||||
from app.config import settings
|
||||
|
||||
router = APIRouter(prefix="/api/documents", tags=["documents"])
|
||||
|
||||
@router.post("/scan", response_model=DocumentResponse)
|
||||
async def scan_document(
|
||||
req: ScanRequest,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
# Check scan limit for free users
|
||||
if user.plan == "free" and user.scan_count_today >= settings.FREE_SCAN_LIMIT:
|
||||
raise HTTPException(status_code=429, detail="Limite de scans diários atingido. Faça upgrade para Premium.")
|
||||
|
||||
# Calculate file size
|
||||
image_data = req.image
|
||||
if "," in image_data:
|
||||
image_data_clean = image_data.split(",", 1)[1]
|
||||
else:
|
||||
image_data_clean = image_data
|
||||
file_size = len(base64.b64decode(image_data_clean))
|
||||
|
||||
# AI analysis
|
||||
try:
|
||||
result = await analyze_document(req.image)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Erro na análise IA: {str(e)}")
|
||||
|
||||
# Save document
|
||||
doc = Document(
|
||||
user_id=user.id,
|
||||
title=result.get("title", "Documento sem título"),
|
||||
category=result.get("category", "outro"),
|
||||
original_image=req.image,
|
||||
extracted_text=result.get("extracted_text", ""),
|
||||
summary=result.get("summary", ""),
|
||||
extracted_data=result.get("extracted_data", {}),
|
||||
risk_alerts=result.get("risk_alerts", []),
|
||||
tags=result.get("tags", []),
|
||||
file_size=file_size
|
||||
)
|
||||
db.add(doc)
|
||||
|
||||
# Update scan count
|
||||
user.scan_count_today += 1
|
||||
await db.commit()
|
||||
await db.refresh(doc)
|
||||
|
||||
return DocumentResponse(
|
||||
id=doc.id, title=doc.title, category=doc.category,
|
||||
extracted_text=doc.extracted_text, summary=doc.summary,
|
||||
extracted_data=doc.extracted_data, risk_alerts=doc.risk_alerts,
|
||||
tags=doc.tags, file_size=doc.file_size, created_at=doc.created_at
|
||||
)
|
||||
|
||||
@router.get("/", response_model=DocumentListResponse)
|
||||
async def list_documents(
|
||||
search: Optional[str] = None,
|
||||
category: Optional[str] = None,
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
query = select(Document).where(Document.user_id == user.id)
|
||||
count_query = select(func.count(Document.id)).where(Document.user_id == user.id)
|
||||
|
||||
if category:
|
||||
query = query.where(Document.category == category)
|
||||
count_query = count_query.where(Document.category == category)
|
||||
if search:
|
||||
search_filter = Document.extracted_text.ilike(f"%{search}%")
|
||||
query = query.where(search_filter)
|
||||
count_query = count_query.where(search_filter)
|
||||
|
||||
total = (await db.execute(count_query)).scalar()
|
||||
result = await db.execute(query.order_by(desc(Document.created_at)).offset((page-1)*limit).limit(limit))
|
||||
docs = result.scalars().all()
|
||||
|
||||
return DocumentListResponse(
|
||||
documents=[DocumentResponse(
|
||||
id=d.id, title=d.title, category=d.category,
|
||||
extracted_text=d.extracted_text, summary=d.summary,
|
||||
extracted_data=d.extracted_data, risk_alerts=d.risk_alerts,
|
||||
tags=d.tags, file_size=d.file_size, created_at=d.created_at
|
||||
) for d in docs],
|
||||
total=total
|
||||
)
|
||||
|
||||
@router.get("/{doc_id}", response_model=DocumentResponse)
|
||||
async def get_document(
|
||||
doc_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
result = await db.execute(select(Document).where(Document.id == doc_id, Document.user_id == user.id))
|
||||
doc = result.scalar_one_or_none()
|
||||
if not doc:
|
||||
raise HTTPException(status_code=404, detail="Documento não encontrado")
|
||||
return DocumentResponse(
|
||||
id=doc.id, title=doc.title, category=doc.category,
|
||||
extracted_text=doc.extracted_text, summary=doc.summary,
|
||||
extracted_data=doc.extracted_data, risk_alerts=doc.risk_alerts,
|
||||
tags=doc.tags, file_size=doc.file_size, created_at=doc.created_at
|
||||
)
|
||||
|
||||
@router.get("/{doc_id}/image")
|
||||
async def get_document_image(
|
||||
doc_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
result = await db.execute(select(Document).where(Document.id == doc_id, Document.user_id == user.id))
|
||||
doc = result.scalar_one_or_none()
|
||||
if not doc:
|
||||
raise HTTPException(status_code=404, detail="Documento não encontrado")
|
||||
return {"image": doc.original_image}
|
||||
|
||||
@router.delete("/{doc_id}")
|
||||
async def delete_document(
|
||||
doc_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
result = await db.execute(select(Document).where(Document.id == doc_id, Document.user_id == user.id))
|
||||
doc = result.scalar_one_or_none()
|
||||
if not doc:
|
||||
raise HTTPException(status_code=404, detail="Documento não encontrado")
|
||||
await db.delete(doc)
|
||||
await db.commit()
|
||||
return {"message": "Documento excluído"}
|
||||
Reference in New Issue
Block a user