📚 Documentação inicial do ALETHEIA
- MANUAL-PRODUTO.md: Manual do usuário final - MANUAL-VENDAS.md: Estratégia comercial e vendas - MANUAL-TECNICO.md: Infraestrutura e deploy - README.md: Visão geral do projeto
This commit is contained in:
0
backend/app/__init__.py
Normal file
0
backend/app/__init__.py
Normal file
BIN
backend/app/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/app/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/__pycache__/config.cpython-312.pyc
Normal file
BIN
backend/app/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/__pycache__/database.cpython-312.pyc
Normal file
BIN
backend/app/__pycache__/database.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/__pycache__/main.cpython-312.pyc
Normal file
BIN
backend/app/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
15
backend/app/config.py
Normal file
15
backend/app/config.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
class Settings(BaseSettings):
|
||||
DATABASE_URL: str = "sqlite+aiosqlite:///./aletheia.db"
|
||||
SECRET_KEY: str = "aletheia-secret-key-change-in-production"
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24
|
||||
OPENAI_API_KEY: str = ""
|
||||
OPENAI_MODEL: str = "gpt-4o-mini"
|
||||
FREE_SCAN_LIMIT: int = 3
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
settings = Settings()
|
||||
17
backend/app/database.py
Normal file
17
backend/app/database.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from app.config import settings
|
||||
|
||||
engine = create_async_engine(settings.DATABASE_URL, echo=False)
|
||||
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
async def get_db():
|
||||
async with async_session() as session:
|
||||
yield session
|
||||
|
||||
async def init_db():
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
Binary file not shown.
Binary file not shown.
27
backend/app/integrations/open_food_facts.py
Normal file
27
backend/app/integrations/open_food_facts.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import httpx
|
||||
from typing import Optional
|
||||
|
||||
async def fetch_product(barcode: str) -> Optional[dict]:
|
||||
url = f"https://world.openfoodfacts.org/api/v2/product/{barcode}.json"
|
||||
headers = {"User-Agent": "Aletheia/1.0 (contato@aletheia.app)"}
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
resp = await client.get(url, headers=headers)
|
||||
if resp.status_code != 200:
|
||||
return None
|
||||
data = resp.json()
|
||||
if data.get("status") != 1:
|
||||
return None
|
||||
p = data["product"]
|
||||
return {
|
||||
"name": p.get("product_name") or p.get("product_name_pt") or "Produto desconhecido",
|
||||
"brand": p.get("brands", ""),
|
||||
"category": p.get("categories", ""),
|
||||
"ingredients_text": p.get("ingredients_text") or p.get("ingredients_text_pt") or "",
|
||||
"nutri_score": (p.get("nutriscore_grade") or "").lower(),
|
||||
"nova_group": p.get("nova_group"),
|
||||
"nutrition": p.get("nutriments", {}),
|
||||
"image_url": p.get("image_url", ""),
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
86
backend/app/integrations/openai_client.py
Normal file
86
backend/app/integrations/openai_client.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import json
|
||||
from openai import AsyncOpenAI
|
||||
from app.config import settings
|
||||
|
||||
SYSTEM_PROMPT = """Você é um nutricionista especialista brasileiro que analisa rótulos de alimentos.
|
||||
Responda SEMPRE em JSON válido com esta estrutura exata:
|
||||
{
|
||||
"score": <int 0-100>,
|
||||
"summary": "<resumo em 2-3 frases para leigo, em português>",
|
||||
"positives": ["<ponto positivo 1>", ...],
|
||||
"negatives": ["<ponto negativo 1>", ...],
|
||||
"ingredients": [
|
||||
{
|
||||
"name": "<nome no rótulo>",
|
||||
"popular_name": "<nome popular ou null>",
|
||||
"explanation": "<o que é, 1 frase>",
|
||||
"classification": "<good|warning|bad>",
|
||||
"reason": "<motivo da classificação, 1 frase>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Critérios para o score:
|
||||
- 90-100: Alimento natural, minimamente processado, sem aditivos
|
||||
- 70-89: Bom, com poucos aditivos ou processamento leve
|
||||
- 50-69: Médio, processado mas aceitável com moderação
|
||||
- 30-49: Ruim, ultraprocessado com vários aditivos
|
||||
- 0-29: Péssimo, alto em açúcar/sódio/gordura trans, muitos aditivos
|
||||
|
||||
Considere Nutri-Score, classificação NOVA, e ingredientes problemáticos.
|
||||
Seja direto e honesto. Use linguagem simples."""
|
||||
|
||||
async def analyze_product(product_data: dict) -> dict:
|
||||
if not settings.OPENAI_API_KEY:
|
||||
return _mock_analysis(product_data)
|
||||
|
||||
client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
|
||||
|
||||
user_msg = f"""Produto: {product_data.get('name', 'Desconhecido')}
|
||||
Marca: {product_data.get('brand', '')}
|
||||
Categoria: {product_data.get('category', '')}
|
||||
Ingredientes: {product_data.get('ingredients_text', 'Não disponível')}
|
||||
Nutri-Score: {product_data.get('nutri_score', 'N/A')}
|
||||
NOVA: {product_data.get('nova_group', 'N/A')}
|
||||
|
||||
Analise este produto."""
|
||||
|
||||
try:
|
||||
resp = await client.chat.completions.create(
|
||||
model=settings.OPENAI_MODEL,
|
||||
messages=[
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_msg}
|
||||
],
|
||||
response_format={"type": "json_object"},
|
||||
temperature=0.3,
|
||||
timeout=15,
|
||||
)
|
||||
return json.loads(resp.choices[0].message.content)
|
||||
except Exception as e:
|
||||
print(f"OpenAI error: {e}")
|
||||
return _mock_analysis(product_data)
|
||||
|
||||
def _mock_analysis(product_data: dict) -> dict:
|
||||
ingredients = product_data.get("ingredients_text", "")
|
||||
score = 50
|
||||
if any(w in ingredients.lower() for w in ["açúcar", "sugar", "xarope", "glucose"]):
|
||||
score -= 15
|
||||
if any(w in ingredients.lower() for w in ["hidrogenada", "trans"]):
|
||||
score -= 20
|
||||
if product_data.get("nova_group") == 4:
|
||||
score -= 10
|
||||
ns = product_data.get("nutri_score", "")
|
||||
if ns == "e": score -= 10
|
||||
elif ns == "d": score -= 5
|
||||
elif ns == "a": score += 15
|
||||
elif ns == "b": score += 10
|
||||
score = max(0, min(100, score))
|
||||
|
||||
return {
|
||||
"score": score,
|
||||
"summary": f"Análise baseada em regras para {product_data.get('name', 'este produto')}. Configure OPENAI_API_KEY para análise completa com IA.",
|
||||
"positives": ["Dados nutricionais disponíveis"],
|
||||
"negatives": ["Análise IA indisponível - usando fallback"],
|
||||
"ingredients": []
|
||||
}
|
||||
27
backend/app/main.py
Normal file
27
backend/app/main.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
from app.database import init_db
|
||||
from app.routers import auth, scan
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
await init_db()
|
||||
yield
|
||||
|
||||
app = FastAPI(title="Aletheia API", version="0.1.0", lifespan=lifespan)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:3080", "http://127.0.0.1:3080"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(auth.router)
|
||||
app.include_router(scan.router)
|
||||
|
||||
@app.get("/api/health")
|
||||
async def health():
|
||||
return {"status": "ok", "app": "Aletheia API v0.1"}
|
||||
5
backend/app/models/__init__.py
Normal file
5
backend/app/models/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from app.models.user import User
|
||||
from app.models.product import Product
|
||||
from app.models.scan import Scan
|
||||
|
||||
__all__ = ["User", "Product", "Scan"]
|
||||
BIN
backend/app/models/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/product.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/product.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/scan.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/scan.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/user.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/user.cpython-312.pyc
Normal file
Binary file not shown.
17
backend/app/models/product.py
Normal file
17
backend/app/models/product.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime
|
||||
from datetime import datetime, timezone
|
||||
from app.database import Base
|
||||
|
||||
class Product(Base):
|
||||
__tablename__ = "products"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
barcode = Column(String, unique=True, index=True, nullable=False)
|
||||
name = Column(String)
|
||||
brand = Column(String)
|
||||
category = Column(String)
|
||||
ingredients_text = Column(Text)
|
||||
nutri_score = Column(String)
|
||||
nova_group = Column(Integer)
|
||||
nutrition_json = Column(Text) # JSON string
|
||||
image_url = Column(String)
|
||||
updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
||||
15
backend/app/models/scan.py
Normal file
15
backend/app/models/scan.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime
|
||||
from datetime import datetime, timezone
|
||||
from app.database import Base
|
||||
|
||||
class Scan(Base):
|
||||
__tablename__ = "scans"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
barcode = Column(String, nullable=False)
|
||||
product_name = Column(String)
|
||||
brand = Column(String)
|
||||
score = Column(Integer)
|
||||
summary = Column(Text)
|
||||
analysis_json = Column(Text) # Full AI analysis as JSON
|
||||
scanned_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
||||
12
backend/app/models/user.py
Normal file
12
backend/app/models/user.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime
|
||||
from datetime import datetime, timezone
|
||||
from app.database import Base
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, index=True, nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
password_hash = Column(String, nullable=False)
|
||||
is_premium = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
||||
0
backend/app/routers/__init__.py
Normal file
0
backend/app/routers/__init__.py
Normal file
BIN
backend/app/routers/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/app/routers/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/auth.cpython-312.pyc
Normal file
BIN
backend/app/routers/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/scan.cpython-312.pyc
Normal file
BIN
backend/app/routers/__pycache__/scan.cpython-312.pyc
Normal file
Binary file not shown.
43
backend/app/routers/auth.py
Normal file
43
backend/app/routers/auth.py
Normal file
@@ -0,0 +1,43 @@
|
||||
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"])
|
||||
|
||||
@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, 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={"id": user.id, "email": user.email, "name": user.name, "is_premium": user.is_premium}
|
||||
)
|
||||
|
||||
@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={"id": user.id, "email": user.email, "name": user.name, "is_premium": user.is_premium}
|
||||
)
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
async def me(user: User = Depends(get_current_user)):
|
||||
return UserResponse(id=user.id, email=user.email, name=user.name, is_premium=user.is_premium)
|
||||
106
backend/app/routers/scan.py
Normal file
106
backend/app/routers/scan.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import json
|
||||
from datetime import datetime, timezone, date
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.models.product import Product
|
||||
from app.models.scan import Scan
|
||||
from app.schemas.scan import ScanRequest, ScanResult, ScanHistoryItem
|
||||
from app.utils.security import get_current_user
|
||||
from app.integrations.open_food_facts import fetch_product
|
||||
from app.integrations.openai_client import analyze_product
|
||||
from app.config import settings
|
||||
from app.services.seed import SEED_PRODUCTS
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["scan"])
|
||||
|
||||
@router.post("/scan", response_model=ScanResult)
|
||||
async def scan_product(req: ScanRequest, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
# Rate limit check
|
||||
if not user.is_premium:
|
||||
today_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
result = await db.execute(
|
||||
select(func.count(Scan.id)).where(Scan.user_id == user.id, Scan.scanned_at >= today_start)
|
||||
)
|
||||
count = result.scalar()
|
||||
if count >= settings.FREE_SCAN_LIMIT:
|
||||
raise HTTPException(status_code=429, detail=f"Limite de {settings.FREE_SCAN_LIMIT} scans/dia atingido. Faça upgrade para Premium!")
|
||||
|
||||
# Check local cache
|
||||
result = await db.execute(select(Product).where(Product.barcode == req.barcode))
|
||||
product = result.scalar_one_or_none()
|
||||
|
||||
product_data = None
|
||||
source = "cache"
|
||||
|
||||
if product:
|
||||
product_data = {
|
||||
"name": product.name, "brand": product.brand, "category": product.category,
|
||||
"ingredients_text": product.ingredients_text, "nutri_score": product.nutri_score,
|
||||
"nova_group": product.nova_group, "nutrition": json.loads(product.nutrition_json or "{}"),
|
||||
"image_url": product.image_url,
|
||||
}
|
||||
else:
|
||||
# Check seed data
|
||||
if req.barcode in SEED_PRODUCTS:
|
||||
product_data = SEED_PRODUCTS[req.barcode].copy()
|
||||
source = "seed"
|
||||
else:
|
||||
# Fetch from Open Food Facts
|
||||
product_data = await fetch_product(req.barcode)
|
||||
source = "open_food_facts"
|
||||
|
||||
if product_data:
|
||||
new_product = Product(
|
||||
barcode=req.barcode, name=product_data.get("name"), brand=product_data.get("brand"),
|
||||
category=product_data.get("category"), ingredients_text=product_data.get("ingredients_text"),
|
||||
nutri_score=product_data.get("nutri_score"), nova_group=product_data.get("nova_group"),
|
||||
nutrition_json=json.dumps(product_data.get("nutrition", {})),
|
||||
image_url=product_data.get("image_url", ""),
|
||||
)
|
||||
db.add(new_product)
|
||||
await db.commit()
|
||||
|
||||
if not product_data:
|
||||
raise HTTPException(status_code=404, detail="Produto não encontrado. Tente inserir manualmente.")
|
||||
|
||||
# AI Analysis
|
||||
analysis = await analyze_product(product_data)
|
||||
|
||||
# Save scan
|
||||
scan = Scan(
|
||||
user_id=user.id, barcode=req.barcode, product_name=product_data.get("name"),
|
||||
brand=product_data.get("brand"), score=analysis.get("score", 50),
|
||||
summary=analysis.get("summary", ""), analysis_json=json.dumps(analysis),
|
||||
)
|
||||
db.add(scan)
|
||||
await db.commit()
|
||||
|
||||
return ScanResult(
|
||||
barcode=req.barcode,
|
||||
product_name=product_data.get("name"),
|
||||
brand=product_data.get("brand"),
|
||||
category=product_data.get("category"),
|
||||
image_url=product_data.get("image_url"),
|
||||
score=analysis.get("score", 50),
|
||||
summary=analysis.get("summary", ""),
|
||||
positives=analysis.get("positives", []),
|
||||
negatives=analysis.get("negatives", []),
|
||||
ingredients=analysis.get("ingredients", []),
|
||||
nutri_score=product_data.get("nutri_score"),
|
||||
nova_group=product_data.get("nova_group"),
|
||||
source=source,
|
||||
)
|
||||
|
||||
@router.get("/history", response_model=list[ScanHistoryItem])
|
||||
async def get_history(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(
|
||||
select(Scan).where(Scan.user_id == user.id).order_by(Scan.scanned_at.desc()).limit(50)
|
||||
)
|
||||
scans = result.scalars().all()
|
||||
return [ScanHistoryItem(
|
||||
id=s.id, barcode=s.barcode, product_name=s.product_name,
|
||||
brand=s.brand, score=s.score, scanned_at=s.scanned_at
|
||||
) for s in scans]
|
||||
0
backend/app/schemas/__init__.py
Normal file
0
backend/app/schemas/__init__.py
Normal file
BIN
backend/app/schemas/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/auth.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/scan.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/scan.cpython-312.pyc
Normal file
Binary file not shown.
21
backend/app/schemas/auth.py
Normal file
21
backend/app/schemas/auth.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from pydantic import BaseModel, EmailStr
|
||||
|
||||
class RegisterRequest(BaseModel):
|
||||
email: str
|
||||
name: str
|
||||
password: str
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
email: str
|
||||
password: str
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
user: dict
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
id: int
|
||||
email: str
|
||||
name: str
|
||||
is_premium: bool
|
||||
36
backend/app/schemas/scan.py
Normal file
36
backend/app/schemas/scan.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
class ScanRequest(BaseModel):
|
||||
barcode: str
|
||||
|
||||
class IngredientAnalysis(BaseModel):
|
||||
name: str
|
||||
popular_name: Optional[str] = None
|
||||
explanation: str
|
||||
classification: str # "good", "warning", "bad"
|
||||
reason: str
|
||||
|
||||
class ScanResult(BaseModel):
|
||||
barcode: str
|
||||
product_name: Optional[str] = None
|
||||
brand: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
image_url: Optional[str] = None
|
||||
score: int
|
||||
summary: str
|
||||
positives: List[str]
|
||||
negatives: List[str]
|
||||
ingredients: List[IngredientAnalysis]
|
||||
nutri_score: Optional[str] = None
|
||||
nova_group: Optional[int] = None
|
||||
source: str = "open_food_facts"
|
||||
|
||||
class ScanHistoryItem(BaseModel):
|
||||
id: int
|
||||
barcode: str
|
||||
product_name: Optional[str] = None
|
||||
brand: Optional[str] = None
|
||||
score: int
|
||||
scanned_at: datetime
|
||||
0
backend/app/services/__init__.py
Normal file
0
backend/app/services/__init__.py
Normal file
BIN
backend/app/services/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/app/services/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/services/__pycache__/seed.cpython-312.pyc
Normal file
BIN
backend/app/services/__pycache__/seed.cpython-312.pyc
Normal file
Binary file not shown.
103
backend/app/services/seed.py
Normal file
103
backend/app/services/seed.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# Seed data - Brazilian popular products
|
||||
SEED_PRODUCTS = {
|
||||
"7891000100103": {
|
||||
"name": "Coca-Cola Original 350ml",
|
||||
"brand": "Coca-Cola",
|
||||
"category": "Refrigerantes",
|
||||
"ingredients_text": "Água gaseificada, açúcar, extrato de noz de cola, cafeína, corante caramelo IV (INS 150d), acidulante ácido fosfórico (INS 338), aroma natural.",
|
||||
"nutri_score": "e",
|
||||
"nova_group": 4,
|
||||
"nutrition": {"energy_kcal": 42, "sugars": 10.6, "fat": 0, "saturated_fat": 0, "sodium": 0.01, "fiber": 0, "proteins": 0},
|
||||
"image_url": "",
|
||||
},
|
||||
"7891000053508": {
|
||||
"name": "Nescau 2.0",
|
||||
"brand": "Nestlé",
|
||||
"category": "Achocolatados",
|
||||
"ingredients_text": "Açúcar, cacau em pó, maltodextrina, composto lácteo (soro de leite em pó e gordura vegetal), minerais (ferro, zinco), sal, vitaminas (C, B3, B5, B6, A, ácido fólico, D, B12), emulsificante lecitina de soja, aromatizante.",
|
||||
"nutri_score": "d",
|
||||
"nova_group": 4,
|
||||
"nutrition": {"energy_kcal": 380, "sugars": 72, "fat": 3.5, "saturated_fat": 2, "sodium": 0.1, "fiber": 3.5, "proteins": 5},
|
||||
"image_url": "",
|
||||
},
|
||||
"7891000305232": {
|
||||
"name": "Miojo Lámen Galinha Caipira",
|
||||
"brand": "Nissin",
|
||||
"category": "Macarrão instantâneo",
|
||||
"ingredients_text": "Farinha de trigo enriquecida com ferro e ácido fólico, gordura vegetal, sal, reguladores de acidez (carbonato de potássio e carbonato de sódio), espessante goma guar, corante natural de cúrcuma, realçador de sabor glutamato monossódico, aromas, açúcar, cebola, alho, salsa.",
|
||||
"nutri_score": "d",
|
||||
"nova_group": 4,
|
||||
"nutrition": {"energy_kcal": 436, "sugars": 4, "fat": 17, "saturated_fat": 8, "sodium": 1.6, "fiber": 2, "proteins": 9},
|
||||
"image_url": "",
|
||||
},
|
||||
"7891962057620": {
|
||||
"name": "Toddy Original",
|
||||
"brand": "PepsiCo",
|
||||
"category": "Achocolatados",
|
||||
"ingredients_text": "Açúcar, cacau em pó, maltodextrina, minerais (fosfato de cálcio, pirofosfato férrico, óxido de zinco), vitaminas (C, PP, B6, B2, B1, A, ácido fólico, D, B12), sal, emulsificante lecitina de soja e aromatizante.",
|
||||
"nutri_score": "d",
|
||||
"nova_group": 4,
|
||||
"nutrition": {"energy_kcal": 375, "sugars": 77, "fat": 2.2, "saturated_fat": 1.4, "sodium": 0.05, "fiber": 4.5, "proteins": 3.9},
|
||||
"image_url": "",
|
||||
},
|
||||
"7891910000197": {
|
||||
"name": "Guaraná Antarctica 350ml",
|
||||
"brand": "Ambev",
|
||||
"category": "Refrigerantes",
|
||||
"ingredients_text": "Água gaseificada, açúcar, extrato de guaraná, acidulante ácido cítrico, aroma natural e corante caramelo IV.",
|
||||
"nutri_score": "e",
|
||||
"nova_group": 4,
|
||||
"nutrition": {"energy_kcal": 40, "sugars": 10, "fat": 0, "saturated_fat": 0, "sodium": 0.01, "fiber": 0, "proteins": 0},
|
||||
"image_url": "",
|
||||
},
|
||||
"7891150029392": {
|
||||
"name": "Leite Moça Tradicional 395g",
|
||||
"brand": "Nestlé",
|
||||
"category": "Leite condensado",
|
||||
"ingredients_text": "Leite integral, açúcar e lactose.",
|
||||
"nutri_score": "d",
|
||||
"nova_group": 2,
|
||||
"nutrition": {"energy_kcal": 321, "sugars": 55, "fat": 8, "saturated_fat": 5, "sodium": 0.12, "fiber": 0, "proteins": 7},
|
||||
"image_url": "",
|
||||
},
|
||||
"7896004800011": {
|
||||
"name": "Biscoito Maizena Vitarella",
|
||||
"brand": "Vitarella",
|
||||
"category": "Biscoitos",
|
||||
"ingredients_text": "Farinha de trigo enriquecida com ferro e ácido fólico, açúcar, gordura vegetal, amido de milho, soro de leite em pó, sal, fermentos químicos (bicarbonato de amônio, bicarbonato de sódio e fosfato monocálcico), emulsificante lecitina de soja, aromatizante.",
|
||||
"nutri_score": "d",
|
||||
"nova_group": 4,
|
||||
"nutrition": {"energy_kcal": 443, "sugars": 22, "fat": 14, "saturated_fat": 6, "sodium": 0.3, "fiber": 1.5, "proteins": 7},
|
||||
"image_url": "",
|
||||
},
|
||||
"7891000244203": {
|
||||
"name": "Iogurte Nestlé Grego Tradicional",
|
||||
"brand": "Nestlé",
|
||||
"category": "Iogurtes",
|
||||
"ingredients_text": "Leite integral e/ou reconstituído, preparado de açúcar (açúcar, água, amido modificado, cloreto de cálcio e conservador sorbato de potássio), creme de leite, proteínas do leite, fermento lácteo.",
|
||||
"nutri_score": "c",
|
||||
"nova_group": 4,
|
||||
"nutrition": {"energy_kcal": 134, "sugars": 15, "fat": 6, "saturated_fat": 3.8, "sodium": 0.05, "fiber": 0, "proteins": 4.8},
|
||||
"image_url": "",
|
||||
},
|
||||
"7622300830236": {
|
||||
"name": "Biscoito Oreo Original",
|
||||
"brand": "Mondelez",
|
||||
"category": "Biscoitos recheados",
|
||||
"ingredients_text": "Farinha de trigo enriquecida com ferro e ácido fólico, açúcar, gordura vegetal, cacau em pó, amido de milho, sal, fermentos químicos, emulsificante lecitina de soja, aromatizante vanilina.",
|
||||
"nutri_score": "e",
|
||||
"nova_group": 4,
|
||||
"nutrition": {"energy_kcal": 476, "sugars": 36, "fat": 20, "saturated_fat": 10, "sodium": 0.4, "fiber": 2, "proteins": 5},
|
||||
"image_url": "",
|
||||
},
|
||||
"7891000362006": {
|
||||
"name": "Aveia Quaker Flocos Finos",
|
||||
"brand": "Quaker",
|
||||
"category": "Cereais",
|
||||
"ingredients_text": "Aveia em flocos finos.",
|
||||
"nutri_score": "a",
|
||||
"nova_group": 1,
|
||||
"nutrition": {"energy_kcal": 366, "sugars": 1, "fat": 7.5, "saturated_fat": 1.3, "sodium": 0.002, "fiber": 9.5, "proteins": 14},
|
||||
"image_url": "",
|
||||
},
|
||||
}
|
||||
BIN
backend/app/utils/__pycache__/security.cpython-312.pyc
Normal file
BIN
backend/app/utils/__pycache__/security.cpython-312.pyc
Normal file
Binary file not shown.
43
backend/app/utils/security.py
Normal file
43
backend/app/utils/security.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from passlib.context import CryptContext
|
||||
from jose import jwt, JWTError
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.config import settings
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
security = HTTPBearer()
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def verify_password(plain: str, hashed: str) -> bool:
|
||||
return pwd_context.verify(plain, hashed)
|
||||
|
||||
def create_access_token(data: dict) -> str:
|
||||
to_encode = data.copy()
|
||||
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
to_encode.update({"exp": expire})
|
||||
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
|
||||
async def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
) -> User:
|
||||
try:
|
||||
payload = jwt.decode(credentials.credentials, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
user_id = payload.get("sub")
|
||||
if user_id is None:
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
except JWTError:
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
|
||||
result = await db.execute(select(User).where(User.id == int(user_id)))
|
||||
user = result.scalar_one_or_none()
|
||||
if user is None:
|
||||
raise HTTPException(status_code=401, detail="User not found")
|
||||
return user
|
||||
Reference in New Issue
Block a user