saveinmed/saveinmed-bff
Tiago Yamamoto 851dd4f265 chore: optimize Dockerfiles with multi-stage builds and caching
- Backend (Go): Use scratch image (~5MB), add build cache for modules
- Backoffice (NestJS): Add pnpm cache, alpine image, fix Prisma client copy
- BFF (Python): Add multi-stage with virtualenv, pip cache, optimized env vars
- All: Add non-root users for security
2025-12-18 17:28:52 -03:00
..
docs first commit 2025-12-17 13:58:26 -03:00
scripts first commit 2025-12-17 13:58:26 -03:00
src first commit 2025-12-17 13:58:26 -03:00
tests first commit 2025-12-17 13:58:26 -03:00
.env.example first commit 2025-12-17 13:58:26 -03:00
.gitignore docs: adiciona documentação completa do projeto SaveInMed 2025-12-17 17:07:30 -03:00
Dockerfile chore: optimize Dockerfiles with multi-stage builds and caching 2025-12-18 17:28:52 -03:00
package-lock.json first commit 2025-12-17 13:58:26 -03:00
package.json first commit 2025-12-17 13:58:26 -03:00
README.md first commit 2025-12-17 13:58:26 -03:00
requirements.txt first commit 2025-12-17 13:58:26 -03:00

Save in Med BFF

BFF para a plataforma Save in Med, integrando serviços do Appwrite (Databases) e expondo rotas de categorias, subcategorias, usuários, empresas, endereços, produtos de catálogo, carrinhos, pedidos e pagamentos.

Pré-requisitos

  • Python 3.12+
  • Docker (opcional)

Configuração & Segurança

A aplicação utiliza variáveis de ambiente carregadas via arquivo .env. Utilize o arquivo .env.example como base.

Variáveis obrigatórias:

  • APPWRITE_ENDPOINT: URL do serviço Appwrite.
  • APPWRITE_PROJECT_ID: identificador do projeto Appwrite.
  • APPWRITE_API_KEY: chave da API Appwrite com permissões para Databases.
  • APPWRITE_DATABASE_ID: identificador do banco de dados utilizado.
  • APPWRITE_COLLECTION_PRODUTOS_CATALOGO_ID: identificador da coleção de produtos de catálogo.
  • APPWRITE_COLLECTION_LABORATORIOS_ID: identificador da coleção de laboratórios.
  • APPWRITE_COLLECTION_CATEGORIAS_ID: identificador da coleção de categorias.
  • APPWRITE_COLLECTION_SUBCATEGORIAS_ID: identificador da coleção de subcategorias de produtos.
  • APPWRITE_COLLECTION_CARRINHOS_ID: identificador da coleção de carrinhos.
  • APPWRITE_COLLECTION_PEDIDOS_ID: identificador da coleção de pedidos.
  • APPWRITE_COLLECTION_USUARIOS_ID: identificador da coleção de usuários.
  • APPWRITE_COLLECTION_EMPRESAS_ID: identificador da coleção de empresas.
  • APPWRITE_COLLECTION_EMPRESAS_DADOS_ID: identificador da coleção com os dados enriquecidos de empresas (ReceitaWS).
  • APPWRITE_COLLECTION_ENDERECOS_ID: identificador da coleção de endereços.
  • APPWRITE_COLLECTION_ENTREGAS_ID: identificador da coleção de entregas.
  • MAPBOX_ACCESS_TOKEN: token de acesso para geocodificação automática dos endereços das empresas.

Variáveis opcionais:

  • SECURITY_API_KEY: chave para proteger as rotas com o header x-api-key.
  • CORS_ALLOW_ORIGINS: lista separada por vírgula de origens autorizadas no CORS. Se vazia, * é utilizado.

Campos adicionais na coleção empresas-dados (Appwrite)

Crie os atributos abaixo na coleção empresas-dados para suportar frete e geocodificação automática:

  • tipo-frete (string, ex.: gratis, fixo, por_km)
  • taxa-entrega (double/decimal)
  • valor-frete-km (double/decimal)
  • raio-entrega-km (double/decimal)
  • latitude (double/decimal)
  • longitude (double/decimal)

Os novos campos serão preenchidos automaticamente a partir do endereço relacionado (enderecos).

Variáveis de autenticação e sessão:

  • JWT_SECRET: segredo para assinar/verificar JWT.
  • JWT_ALGORITHM: algoritmo (ex.: HS256).
  • JWT_ACCESS_TTL_SECONDS: validade do token (segundos).
  • SESSION_COOKIE_NAME: nome do cookie httpOnly.
  • COOKIE_SECURE: true para setar cookie com Secure (produção HTTPS).
  • COOKIE_DOMAIN: domínio do cookie (opcional).
  • SUPERADMIN_IDENTIFICADOR e SUPERADMIN_SENHA: credenciais para criar usuário superadmin no startup. A senha é armazenada com bcrypt_sha256, permitindo valores longos (recomendado usar frases secretas robustas). Não utilize chaves de API como senha.

Quando SECURITY_API_KEY está definido com um valor não vazio, todas as rotas passam a exigir o header x-api-key correspondente. Essa validação é feita via middleware, portanto o header não aparece na documentação do Swagger. Caso a variável esteja vazia ou ausente, nenhuma checagem é feita — ideal para ambientes de desenvolvimento.

Endpoints

  • GET /{ "message": "Hello, World!" }
  • GET /healthz{ "status": "ok" }
  • GET /api/v1/ping{ "pong": true }
  • POST /api/v1/auth/register
  • POST /api/v1/auth/login
  • POST /api/v1/auth/logout
  • GET /api/v1/auth/me
  • GET /api/v1/categorias
  • GET /api/v1/subcategorias
  • GET /api/v1/usuarios
  • GET /api/v1/empresas
  • GET /api/v1/enderecos
  • GET /api/v1/produtos-catalogo
  • GET /api/v1/carrinhos
  • GET /api/v1/pedidos

Como rodar em dev

pip install -r requirements.txt
uvicorn src.main:app --reload

# Docker
docker build -t saveinmed-bff .
docker run -p 8000:8000 --env-file .env saveinmed-bff

Testes rápidos com curl

curl http://localhost:8000/
curl http://localhost:8000/healthz
curl http://localhost:8000/api/v1/ping
curl http://localhost:8000/api/v1/carrinhos
curl http://localhost:8000/api/v1/pedidos

Adicione o header -H "x-api-key: <token>" nos exemplos abaixo quando a variável SECURITY_API_KEY estiver configurada.

Auth

# Registro de usuário (senha é armazenada com hash no Appwrite)
curl -X POST "http://localhost:8000/api/v1/auth/register" \
  -H "Content-Type: application/json" \
  -d '{"identificador": "user@example.com", "nome": "Nome do Usuário", "senha": "senha123"}'

# Login retorna o JWT e seta o cookie httpOnly
curl -i -X POST "http://localhost:8000/api/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"identificador": "user@example.com", "senha": "senha123"}'

# Após o login, o cookie é enviado automaticamente pelo navegador/Swagger
curl -b cookiejar.txt -c cookiejar.txt http://localhost:8000/api/v1/auth/me

# Logout remove o cookie httpOnly
curl -X POST http://localhost:8000/api/v1/auth/logout

Rotas públicas: /, /healthz, /api/v1/ping e todas as rotas sob /api/v1/auth/*. As demais rotas exigem autenticação via JWT (Bearer ou cookie httpOnly).

Durante o startup, caso nenhum usuário superadmin exista na coleção, o BFF cria automaticamente um registro utilizando as credenciais definidas em SUPERADMIN_IDENTIFICADOR e SUPERADMIN_SENHA. Se a senha estiver vazia ou muito longa (> 1024 caracteres), o sistema registra um aviso no log e gera uma senha temporária aleatória.

Categorias

curl -X GET "http://localhost:8000/api/v1/categorias?page=1&per_page=20"
curl -X POST "http://localhost:8000/api/v1/categorias" \
  -H "Content-Type: application/json" \
  -d '{"nome": "Dermocosméticos"}'
curl -X GET "http://localhost:8000/api/v1/categorias/cat_1"
curl -X PATCH "http://localhost:8000/api/v1/categorias/cat_1" \
  -H "Content-Type: application/json" \
  -d '{"nome": "Dermocosméticos premium"}'
curl -X DELETE "http://localhost:8000/api/v1/categorias/cat_1"

Subcategorias

curl -X GET "http://localhost:8000/api/v1/subcategorias?page=1&per_page=20"
curl -X POST "http://localhost:8000/api/v1/subcategorias" \
  -H "Content-Type: application/json" \
  -d '{"nome": "Suplementos", "categoriaId": "cat_1"}'
curl -X GET "http://localhost:8000/api/v1/subcategorias/sub_1"
curl -X PATCH "http://localhost:8000/api/v1/subcategorias/sub_1" \
  -H "Content-Type: application/json" \
  -d '{"nome": "Suplementos esportivos"}'
curl -X DELETE "http://localhost:8000/api/v1/subcategorias/sub_1"

Usuarios

curl -X GET "http://localhost:8000/api/v1/usuarios?page=1&per_page=20"
curl -X POST "http://localhost:8000/api/v1/usuarios" \
  -H "Content-Type: application/json" \
  -d '{"identificador": "admin@saveinmed.com", "senha": "senha123", "ativo": true, "superadmin": true, "telefone": "11987654321"}'
curl -X GET "http://localhost:8000/api/v1/usuarios/user_1"
curl -X PATCH "http://localhost:8000/api/v1/usuarios/user_1" \
  -H "Content-Type: application/json" \
  -d '{"ativo": false}'
curl -X DELETE "http://localhost:8000/api/v1/usuarios/user_1"

Empresas

curl -X GET "http://localhost:8000/api/v1/empresas?page=1&per_page=20"
curl -X POST "http://localhost:8000/api/v1/empresas" \
  -H "Content-Type: application/json" \
  -d '{"cnpj": "12.345.678/0001-00", "razaoSocial": "Clínica Alfa LTDA", "nomeFantasia": "Clínica Alfa"}'
curl -X GET "http://localhost:8000/api/v1/empresas/emp_1"
curl -X PATCH "http://localhost:8000/api/v1/empresas/emp_1" \
  -H "Content-Type: application/json" \
  -d '{
    "nome-fantasia": "Clínica Alfa Centro",
    "telefone": "62999999999",
    "email": "contato@clinicaalfa.com"
  }'
curl -X DELETE "http://localhost:8000/api/v1/empresas/emp_1"

Enderecos

curl -X GET "http://localhost:8000/api/v1/enderecos?page=1&per_page=20"
curl -X POST "http://localhost:8000/api/v1/enderecos" \
  -H "Content-Type: application/json" \
  -d '{"titulo": "Matriz", "cep": "12345-678", "cidade": "São Paulo", "estado": "SP"}'
curl -X GET "http://localhost:8000/api/v1/enderecos/end_1"
curl -X PATCH "http://localhost:8000/api/v1/enderecos/end_1" \
  -H "Content-Type: application/json" \
  -d '{"numero": "456"}'
curl -X DELETE "http://localhost:8000/api/v1/enderecos/end_1"

Estrutura do projeto

.
├─ src/
│  ├─ __init__.py
│  ├─ main.py
│  ├─ app/
│  │  ├─ __init__.py
│  │  ├─ main.py
│  │  └─ api/
│  │     ├─ __init__.py
│  │     └─ v1/
│  │        ├─ __init__.py
│  │        └─ routes.py
│  ├─ core/
│  │  ├─ __init__.py
│  │  ├─ appwrite_client.py
│  │  ├─ config.py
│  │  ├─ deps.py
│  │  └─ security.py
│  └─ modules/
│     ├─ __init__.py
│     ├─ categorias/
│     │  ├─ __init__.py
│     │  ├─ router.py
│     │  ├─ schemas.py
│     │  └─ service.py
│     ├─ carrinhos/
│     │  ├─ __init__.py
│     │  ├─ router.py
│     │  ├─ schemas.py
│     │  └─ service.py
│     ├─ pedidos/
│     │  ├─ __init__.py
│     │  ├─ router.py
│     │  ├─ schemas.py
│     │  └─ service.py
│     └─ produtos_catalogo/
│        ├─ __init__.py
│        ├─ router.py
│        ├─ schemas.py
│        └─ service.py
├─ requirements.txt
├─ Dockerfile
├─ README.md
└─ .gitignore

Próximos passos

  • Criar as relações no Appwrite para os campos usuarios, pedidos, comprador, vendedor e pagamentos das coleções de carrinhos e pedidos. Quando as relações estiverem disponíveis, ajustar o BFF para enviar/receber os objetos de relação em vez de strings.

Licença

MIT