- Optimized Backoffice Dockerfile: node:22-alpine -> gcr.io/distroless/nodejs22-debian12 - Optimized BFF Dockerfile: python:3.12-slim -> gcr.io/distroless/python3-debian12 - Updated Backoffice Auth to use JWT_SECRET for local verification - Updated .env.example with correct env vars |
||
|---|---|---|
| .. | ||
| docs | ||
| scripts | ||
| src | ||
| tests | ||
| .env.example | ||
| .gitignore | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| requirements.txt | ||
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 headerx-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:truepara setar cookie comSecure(produção HTTPS).COOKIE_DOMAIN: domínio do cookie (opcional).SUPERADMIN_IDENTIFICADOReSUPERADMIN_SENHA: credenciais para criar usuário superadmin no startup. A senha é armazenada combcrypt_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/registerPOST /api/v1/auth/loginPOST /api/v1/auth/logoutGET /api/v1/auth/meGET /api/v1/categoriasGET /api/v1/subcategoriasGET /api/v1/usuariosGET /api/v1/empresasGET /api/v1/enderecosGET /api/v1/produtos-catalogoGET /api/v1/carrinhosGET /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ávelSECURITY_API_KEYestiver 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/pinge 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,vendedorepagamentosdas 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