From 16994f5058a5f80744f543f668513998560fae23 Mon Sep 17 00:00:00 2001 From: Davi Aliaga Date: Sat, 10 Oct 2026 00:44:41 -0300 Subject: [PATCH 1/6] fix: enable TLS for Redis connections --- .env.example | 6 +++++- README.md | 8 ++++++-- app/database/redis.py | 9 +++++++-- tests/test_redis_config.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 tests/test_redis_config.py diff --git a/.env.example b/.env.example index 79f56b5..ad37023 100644 --- a/.env.example +++ b/.env.example @@ -8,4 +8,8 @@ ACTA_PG_API_URL=https://acta-pg-api.onrender.com/api/v1 ACTA_IA_TOKEN=defina-um-token-interno REDIS_HOST=localhost -REDIS_PORT=6379 \ No newline at end of file +REDIS_PORT=6379 +REDIS_DB=0 +REDIS_USERNAME= +REDIS_PASSWORD= +REDIS_TLS=false diff --git a/README.md b/README.md index 1e1433f..ab5e90d 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ O worker executa continuamente essa fila, recupera o job, envia o arquivo ao Clo O Redis armazena a fila e o estado necessário para o RQ controlar a execução. Se o processamento falhar, o job possui até três tentativas, com intervalos de 10, 30 e 60 segundos. O arquivo temporário é mantido durante as tentativas e removido quando o processamento termina com sucesso ou falha definitiva. -O Redis precisa estar acessível tanto pela API, que enfileira os jobs, quanto pelo worker, que os consome. Em ambientes com autenticação, configure também `REDIS_PASSWORD` no ambiente da aplicação e do worker. +O Redis precisa estar acessível tanto pela API, que enfileira os jobs, quanto pelo worker, que os consome. Em ambientes com autenticação, configure também `REDIS_PASSWORD` e, quando exigido pelo provedor, `REDIS_USERNAME`. Para serviços Redis que exigem TLS, configure `REDIS_TLS=true`; em Redis local sem TLS, use `false`. As mesmas variáveis devem estar disponíveis para a API e para o worker. ## ✨ Funcionalidades @@ -86,6 +86,10 @@ ACTA_IA_TOKEN=defina-um-token-interno REDIS_HOST=localhost REDIS_PORT=6379 +REDIS_DB=0 +REDIS_USERNAME= +REDIS_PASSWORD= +REDIS_TLS=false ``` Nunca versione senhas, tokens ou credenciais do Cloudinary. O arquivo `.env.example` deve conter somente valores de referência. @@ -282,4 +286,4 @@ Os testes usam dublês para banco, Redis e Cloudinary. Portanto, a suíte valida ## 🤝 Links e autoria - [Repositório](https://github.com/AppActa/acta-import-api) · [Licença MIT](LICENSE) · `acta.institutojef@gmail.com` -- Contribuições: use *issues* e *pull requests*; há um [PULL_REQUEST_TEMPLATE.md](PULL_REQUEST_TEMPLATE.md). \ No newline at end of file +- Contribuições: use *issues* e *pull requests*; há um [PULL_REQUEST_TEMPLATE.md](PULL_REQUEST_TEMPLATE.md). diff --git a/app/database/redis.py b/app/database/redis.py index 1fbfcdb..897b02c 100644 --- a/app/database/redis.py +++ b/app/database/redis.py @@ -4,6 +4,7 @@ from dotenv import load_dotenv from os import getenv from functools import lru_cache +from redis.connection import SSLConnection load_dotenv() @@ -11,13 +12,17 @@ REDIS_PORT = getenv('REDIS_PORT') REDIS_DB = getenv('REDIS_DB') REDIS_PASSWORD = getenv('REDIS_PASSWORD') +REDIS_USERNAME = getenv('REDIS_USERNAME') +REDIS_TLS = getenv('REDIS_TLS', 'false').strip().lower() == 'true' @lru_cache # apenas um pool por sessão def get_pool() -> ConnectionPool: - return ConnectionPool(host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB, password=REDIS_PASSWORD, max_connections=20, retry_on_timeout=True) + # Usa ssl connection quando TLS foi habilitado + connection_options = {'connection_class': SSLConnection} if REDIS_TLS else {} + return ConnectionPool(host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB, username=REDIS_USERNAME, password=REDIS_PASSWORD, max_connections=20, retry_on_timeout=True, **connection_options) def get_conn() -> Redis: try: return Redis(connection_pool=get_pool()) except RedisError as re: - raise HTTPException(status_code=503, detail='Redis indisponível') \ No newline at end of file + raise HTTPException(status_code=503, detail='Redis indisponível') diff --git a/tests/test_redis_config.py b/tests/test_redis_config.py new file mode 100644 index 0000000..14f9f6a --- /dev/null +++ b/tests/test_redis_config.py @@ -0,0 +1,30 @@ +import importlib + +from redis.connection import SSLConnection + +import app.database.redis as redis_database + + +def test_redis_tls_defaults_to_disabled_when_unset(monkeypatch): + monkeypatch.delenv("REDIS_TLS", raising=False) + + module = importlib.reload(redis_database) + + assert module.REDIS_TLS is False + + +def test_redis_pool_uses_ssl_connection_when_tls_enabled(monkeypatch): + monkeypatch.setattr(redis_database, "REDIS_HOST", "redis.example.test") + monkeypatch.setattr(redis_database, "REDIS_PORT", "6379") + monkeypatch.setattr(redis_database, "REDIS_DB", "0") + monkeypatch.setattr(redis_database, "REDIS_USERNAME", "default") + monkeypatch.setattr(redis_database, "REDIS_PASSWORD", "not-a-real-secret") + monkeypatch.setattr(redis_database, "REDIS_TLS", True) + redis_database.get_pool.cache_clear() + + try: + pool = redis_database.get_pool() + assert pool.connection_class is SSLConnection + assert pool.connection_kwargs["username"] == "default" + finally: + redis_database.get_pool.cache_clear() From f18678d168a3e384f77abf59dbe23e6fd674bd3d Mon Sep 17 00:00:00 2001 From: Davi Aliaga Date: Sat, 10 Oct 2026 00:51:11 -0300 Subject: [PATCH 2/6] fix: normalize empty Redis username --- app/database/redis.py | 2 +- tests/test_redis_config.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/database/redis.py b/app/database/redis.py index 897b02c..e6777ea 100644 --- a/app/database/redis.py +++ b/app/database/redis.py @@ -12,7 +12,7 @@ REDIS_PORT = getenv('REDIS_PORT') REDIS_DB = getenv('REDIS_DB') REDIS_PASSWORD = getenv('REDIS_PASSWORD') -REDIS_USERNAME = getenv('REDIS_USERNAME') +REDIS_USERNAME = (getenv('REDIS_USERNAME') or '').strip() or None REDIS_TLS = getenv('REDIS_TLS', 'false').strip().lower() == 'true' @lru_cache # apenas um pool por sessão diff --git a/tests/test_redis_config.py b/tests/test_redis_config.py index 14f9f6a..09d5de3 100644 --- a/tests/test_redis_config.py +++ b/tests/test_redis_config.py @@ -13,6 +13,14 @@ def test_redis_tls_defaults_to_disabled_when_unset(monkeypatch): assert module.REDIS_TLS is False +def test_blank_redis_username_is_treated_as_unset(monkeypatch): + monkeypatch.setenv("REDIS_USERNAME", " ") + + module = importlib.reload(redis_database) + + assert module.REDIS_USERNAME is None + + def test_redis_pool_uses_ssl_connection_when_tls_enabled(monkeypatch): monkeypatch.setattr(redis_database, "REDIS_HOST", "redis.example.test") monkeypatch.setattr(redis_database, "REDIS_PORT", "6379") From 2a408b945fa804b5df8466122ea3b57311659965 Mon Sep 17 00:00:00 2001 From: Davi Aliaga Date: Tue, 13 Oct 2026 20:48:00 -0300 Subject: [PATCH 3/6] =?UTF-8?q?feat/config:=20adiciona=20config=20para=20v?= =?UTF-8?q?ari=C3=A1veis=20de=20ambiente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 8 +------- app/config.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 app/config.py diff --git a/.env.example b/.env.example index ad37023..7a9c212 100644 --- a/.env.example +++ b/.env.example @@ -5,11 +5,5 @@ CLOUDINARY_API_KEY= CLOUDINARY_API_SECRET= ACTA_PG_API_URL=https://acta-pg-api.onrender.com/api/v1 -ACTA_IA_TOKEN=defina-um-token-interno - REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_DB=0 -REDIS_USERNAME= -REDIS_PASSWORD= -REDIS_TLS=false +REDIS_PORT=6379 \ No newline at end of file diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..cdd591b --- /dev/null +++ b/app/config.py @@ -0,0 +1,19 @@ +from os import getenv + +from dotenv import load_dotenv + +load_dotenv() + +DB_URL = getenv('DB_URL') + +CLOUDINARY_CLOUD_NAME = getenv('CLOUDINARY_CLOUD_NAME') +CLOUDINARY_API_KEY = getenv('CLOUDINARY_API_KEY') +CLOUDINARY_API_SECRET = getenv('CLOUDINARY_API_SECRET') + +ACTA_PG_API_URL = getenv('ACTA_PG_API_URL') + +REDIS_HOST = getenv('REDIS_HOST') +REDIS_PORT = getenv('REDIS_PORT') +REDIS_DB = getenv('REDIS_DB') +REDIS_PASSWORD = getenv('REDIS_PASSWORD') +REDIS_SSL = True \ No newline at end of file From d349a81b193fdc48393f06e6f72a9c15636a7e9e Mon Sep 17 00:00:00 2001 From: Davi Aliaga Date: Tue, 13 Oct 2026 20:49:05 -0300 Subject: [PATCH 4/6] refactor/database: remove uso do dotenv e centraliza uso do config --- app/database/postgres.py | 7 +------ app/database/redis.py | 24 +++++++----------------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/app/database/postgres.py b/app/database/postgres.py index f88f4d2..8866a9e 100644 --- a/app/database/postgres.py +++ b/app/database/postgres.py @@ -1,12 +1,7 @@ from contextlib import contextmanager from fastapi import HTTPException -from dotenv import load_dotenv -from os import getenv from psycopg2 import connect - -load_dotenv() - -DB_URL = getenv('DB_URL') +from app.config import DB_URL def _conectar(): return connect(DB_URL) diff --git a/app/database/redis.py b/app/database/redis.py index e6777ea..3410ef6 100644 --- a/app/database/redis.py +++ b/app/database/redis.py @@ -1,28 +1,18 @@ from fastapi import HTTPException -from redis import Redis, ConnectionPool +from redis import Redis, ConnectionPool, SSLConnection from redis.exceptions import RedisError -from dotenv import load_dotenv -from os import getenv from functools import lru_cache -from redis.connection import SSLConnection - -load_dotenv() - -REDIS_HOST = getenv('REDIS_HOST') -REDIS_PORT = getenv('REDIS_PORT') -REDIS_DB = getenv('REDIS_DB') -REDIS_PASSWORD = getenv('REDIS_PASSWORD') -REDIS_USERNAME = (getenv('REDIS_USERNAME') or '').strip() or None -REDIS_TLS = getenv('REDIS_TLS', 'false').strip().lower() == 'true' +from app.config import REDIS_DB, REDIS_HOST, REDIS_PASSWORD, REDIS_PORT, REDIS_SSL @lru_cache # apenas um pool por sessão def get_pool() -> ConnectionPool: - # Usa ssl connection quando TLS foi habilitado - connection_options = {'connection_class': SSLConnection} if REDIS_TLS else {} - return ConnectionPool(host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB, username=REDIS_USERNAME, password=REDIS_PASSWORD, max_connections=20, retry_on_timeout=True, **connection_options) + pool_options = {'host': REDIS_HOST, 'port': REDIS_PORT, 'db': REDIS_DB, 'password': REDIS_PASSWORD, 'max_connections': 20, 'retry_on_timeout': True} + if REDIS_SSL: + pool_options['connection_class'] = SSLConnection + return ConnectionPool(**pool_options) def get_conn() -> Redis: try: return Redis(connection_pool=get_pool()) except RedisError as re: - raise HTTPException(status_code=503, detail='Redis indisponível') + raise HTTPException(status_code=503, detail='Redis indisponível') \ No newline at end of file From 74cb470b59e570d4a9a55225c1b520c9dcc04c07 Mon Sep 17 00:00:00 2001 From: Davi Aliaga Date: Tue, 13 Oct 2026 20:50:36 -0300 Subject: [PATCH 5/6] =?UTF-8?q?refactor:=20remove=20autentica=C3=A7=C3=A3o?= =?UTF-8?q?=20inutlizada=20e=20atualiza=20tratamento=20de=20metadados=20de?= =?UTF-8?q?=20arquivo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/anexos.py | 8 +++---- app/services/anexos.py | 10 ++++++-- app/services/autenticacao.py | 46 ++++++++++-------------------------- app/services/cloudinary.py | 18 +++++++------- tests/test_redis_config.py | 38 ----------------------------- 5 files changed, 33 insertions(+), 87 deletions(-) delete mode 100644 tests/test_redis_config.py diff --git a/app/routes/anexos.py b/app/routes/anexos.py index 86b8405..de57fbc 100644 --- a/app/routes/anexos.py +++ b/app/routes/anexos.py @@ -3,7 +3,7 @@ from app.database.postgres import get_conn from app.schemas import AnexoForm, UsuarioAutenticado from app.services.anexos import consultar_anexo, processar_anexo -from app.services.autenticacao import obter_contexto_ia, obter_usuario_autenticado +from app.services.autenticacao import obter_usuario_autenticado router = APIRouter(prefix='/anexos', tags=['anexos']) @@ -12,13 +12,13 @@ async def post_anexo(arquivo: UploadFile = File(...), dados: AnexoForm = Depends return await processar_anexo(arquivo, dados, conn, usuario) @router.post('/ia', status_code=202) -async def post_anexo_ia(arquivo: UploadFile = File(...), dados: AnexoForm = Depends(), conn=Depends(get_conn), usuario: UsuarioAutenticado = Depends(obter_contexto_ia)): +async def post_anexo_ia(arquivo: UploadFile = File(...), dados: AnexoForm = Depends(), conn=Depends(get_conn), usuario: UsuarioAutenticado = Depends(obter_usuario_autenticado)): return await processar_anexo(arquivo, dados, conn, usuario) @router.get('/ia/{id_anexo}') -def get_anexo_ia(id_anexo: int, conn=Depends(get_conn), usuario: UsuarioAutenticado = Depends(obter_contexto_ia)): +def get_anexo_ia(id_anexo: int, conn=Depends(get_conn), usuario: UsuarioAutenticado = Depends(obter_usuario_autenticado)): return consultar_anexo(conn, id_anexo, usuario.id_empresa) @router.get('/{id_anexo}') def get_anexo(id_anexo: int, conn=Depends(get_conn), usuario: UsuarioAutenticado = Depends(obter_usuario_autenticado)): - return consultar_anexo(conn, id_anexo, usuario.id_empresa) + return consultar_anexo(conn, id_anexo, usuario.id_empresa) \ No newline at end of file diff --git a/app/services/anexos.py b/app/services/anexos.py index d93e348..6df2f19 100644 --- a/app/services/anexos.py +++ b/app/services/anexos.py @@ -76,6 +76,12 @@ def _validar_ciclo_empresa(conn, id_ciclo: int, id_empresa: int) -> None: raise HTTPException(403, 'Ciclo não pertence à empresa do usuário') def _inserir_metadados(conn, arquivo: UploadFile, dados: AnexoForm, extensao: str, usuario: UsuarioAutenticado) -> int: + tipo_arquivo = arquivo.content_type or extensao + + # Corrige mime type genérico + if extensao == 'txt' and tipo_arquivo == 'application/octet-stream': + tipo_arquivo = 'text/plain' + with conn.cursor() as cursor: cursor.execute("SELECT set_config('app.current_user_id', %s, true)", (str(usuario.id_usuario),)) cursor.execute(""" @@ -83,7 +89,7 @@ def _inserir_metadados(conn, arquivo: UploadFile, dados: AnexoForm, extensao: st VALUES (%s, %s, %s, %s, %s, %s, %s, %s, NULL, %s, %s, %s) RETURNING id """, - (usuario.id_empresa, dados.id_ciclo, usuario.id_usuario, dados.id_origem, arquivo.filename, arquivo.content_type or extensao, arquivo.size, 'cloudinary', dados.categoria.value, dados.descricao, Status.PROCESSANDO.value)) + (usuario.id_empresa, dados.id_ciclo, usuario.id_usuario, dados.id_origem, arquivo.filename, tipo_arquivo, arquivo.size, 'cloudinary', dados.categoria.value, dados.descricao, Status.PROCESSANDO.value)) id_anexo = cursor.fetchone()[0] conn.commit() @@ -96,4 +102,4 @@ def _atualizar_status_erro(conn, id_anexo: int, id_usuario: int) -> None: (str(id_usuario),) ) cursor.execute('UPDATE pdca.anexo SET status = %s WHERE id = %s', (Status.ERRO.value, id_anexo)) - conn.commit() + conn.commit() \ No newline at end of file diff --git a/app/services/autenticacao.py b/app/services/autenticacao.py index 73b6588..13f7f4a 100644 --- a/app/services/autenticacao.py +++ b/app/services/autenticacao.py @@ -1,40 +1,10 @@ -from hmac import compare_digest -from os import getenv from httpx import AsyncClient, RequestError -from fastapi import Depends, Header, HTTPException, Security +from fastapi import HTTPException, Security from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer - -from app.database.postgres import get_conn +from app.config import ACTA_PG_API_URL from app.schemas import UsuarioAutenticado - -ACTA_PG_API_URL = getenv('ACTA_PG_API_URL') bearer = HTTPBearer() -async def obter_contexto_ia( - credenciais: HTTPAuthorizationCredentials = Security(bearer), - id_usuario: int = Header(..., alias='X-Acta-Usuario-Id', gt=0), - conn=Depends(get_conn) -) -> UsuarioAutenticado: - token_ia = getenv('ACTA_IA_TOKEN') - if not token_ia or not compare_digest(credenciais.credentials, token_ia): - raise HTTPException(401, 'Token da IA inválido') - - with conn.cursor() as cursor: - cursor.execute( - """ - SELECT id, id_empresa - FROM public.usuario_sistema - WHERE id = %s AND status = 'ATIVO' - """, - (id_usuario,) - ) - usuario = cursor.fetchone() - - if usuario is None: - raise HTTPException(403, 'Usuário da IA não autorizado') - - return UsuarioAutenticado(idUsuario=usuario[0], idEmpresa=usuario[1]) - async def obter_usuario_autenticado(credenciais: HTTPAuthorizationCredentials = Security(bearer)) -> UsuarioAutenticado: headers = {'Authorization': f'Bearer {credenciais.credentials}'} @@ -56,6 +26,16 @@ async def obter_usuario_autenticado(credenciais: HTTPAuthorizationCredentials = raise HTTPException(502, 'Falha ao consultar usuário autenticado') try: - return UsuarioAutenticado.model_validate(resposta.json()) + dados_usuario = resposta.json() + if not isinstance(dados_usuario, dict): + raise ValueError + except (ValueError, TypeError): + raise HTTPException(502, 'Resposta inválida de acta-pg-api') + + if dados_usuario.get('status') != 'ATIVO': + raise HTTPException(403, 'Usuário não autorizado') + + try: + return UsuarioAutenticado.model_validate(dados_usuario) except (ValueError, TypeError): raise HTTPException(502, 'Resposta inválida de acta-pg-api') \ No newline at end of file diff --git a/app/services/cloudinary.py b/app/services/cloudinary.py index 64179b2..a179382 100644 --- a/app/services/cloudinary.py +++ b/app/services/cloudinary.py @@ -1,15 +1,13 @@ -from cloudinary import config +from cloudinary import config as cloudinary_config from cloudinary.uploader import upload -from os import getenv -from dotenv import load_dotenv +from app.config import CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET, CLOUDINARY_CLOUD_NAME -load_dotenv() - -CLOUD_NAME = getenv('CLOUDINARY_CLOUD_NAME') -API_KEY = getenv('CLOUDINARY_API_KEY') -API_SECRET = getenv('CLOUDINARY_API_SECRET') - -config(cloud_name=CLOUD_NAME, api_key=API_KEY, api_secret=API_SECRET, secure=True) +cloudinary_config( + cloud_name=CLOUDINARY_CLOUD_NAME, + api_key=CLOUDINARY_API_KEY, + api_secret=CLOUDINARY_API_SECRET, + secure=True, +) # reforçar tipo raw, cloudinary não sabe lidar com esses arquivos EXTENSOES_RAW = {'docx', 'pptx', 'xlsx', 'csv', 'txt'} diff --git a/tests/test_redis_config.py b/tests/test_redis_config.py deleted file mode 100644 index 09d5de3..0000000 --- a/tests/test_redis_config.py +++ /dev/null @@ -1,38 +0,0 @@ -import importlib - -from redis.connection import SSLConnection - -import app.database.redis as redis_database - - -def test_redis_tls_defaults_to_disabled_when_unset(monkeypatch): - monkeypatch.delenv("REDIS_TLS", raising=False) - - module = importlib.reload(redis_database) - - assert module.REDIS_TLS is False - - -def test_blank_redis_username_is_treated_as_unset(monkeypatch): - monkeypatch.setenv("REDIS_USERNAME", " ") - - module = importlib.reload(redis_database) - - assert module.REDIS_USERNAME is None - - -def test_redis_pool_uses_ssl_connection_when_tls_enabled(monkeypatch): - monkeypatch.setattr(redis_database, "REDIS_HOST", "redis.example.test") - monkeypatch.setattr(redis_database, "REDIS_PORT", "6379") - monkeypatch.setattr(redis_database, "REDIS_DB", "0") - monkeypatch.setattr(redis_database, "REDIS_USERNAME", "default") - monkeypatch.setattr(redis_database, "REDIS_PASSWORD", "not-a-real-secret") - monkeypatch.setattr(redis_database, "REDIS_TLS", True) - redis_database.get_pool.cache_clear() - - try: - pool = redis_database.get_pool() - assert pool.connection_class is SSLConnection - assert pool.connection_kwargs["username"] == "default" - finally: - redis_database.get_pool.cache_clear() From 62ac312957f1efc3d14d01c8375a6095d332a8eb Mon Sep 17 00:00:00 2001 From: Davi Aliaga Date: Tue, 13 Oct 2026 20:59:50 -0300 Subject: [PATCH 6/6] =?UTF-8?q?docs:=20atualiza=20a=20se=C3=A7=C3=A3o=20de?= =?UTF-8?q?=20configura=C3=A7=C3=A3o=20do=20Redis=20no=20README=20para=20r?= =?UTF-8?q?efletir=20a=20obrigatoriedade=20do=20uso=20de=20TLS=20e=20remov?= =?UTF-8?q?e=20vari=C3=A1veis=20desnecess=C3=A1rias?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index ab5e90d..19d0aea 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ O worker executa continuamente essa fila, recupera o job, envia o arquivo ao Clo O Redis armazena a fila e o estado necessário para o RQ controlar a execução. Se o processamento falhar, o job possui até três tentativas, com intervalos de 10, 30 e 60 segundos. O arquivo temporário é mantido durante as tentativas e removido quando o processamento termina com sucesso ou falha definitiva. -O Redis precisa estar acessível tanto pela API, que enfileira os jobs, quanto pelo worker, que os consome. Em ambientes com autenticação, configure também `REDIS_PASSWORD` e, quando exigido pelo provedor, `REDIS_USERNAME`. Para serviços Redis que exigem TLS, configure `REDIS_TLS=true`; em Redis local sem TLS, use `false`. As mesmas variáveis devem estar disponíveis para a API e para o worker. +O Redis precisa estar acessível tanto pela API, que enfileira os jobs, quanto pelo worker, que os consome. Em ambientes com autenticação, configure também `REDIS_PASSWORD`. A conexão usa TLS obrigatoriamente (`REDIS_SSL = True` em `app/config.py`), sem uma variável de ambiente para desativá-lo; use um serviço Redis com TLS habilitado. As configurações de conexão devem estar disponíveis para a API e para o worker. ## ✨ Funcionalidades @@ -82,16 +82,15 @@ CLOUDINARY_API_KEY=sua_api_key CLOUDINARY_API_SECRET=seu_api_secret ACTA_PG_API_URL=https://acta-pg-api.onrender.com/api/v1 -ACTA_IA_TOKEN=defina-um-token-interno REDIS_HOST=localhost REDIS_PORT=6379 REDIS_DB=0 -REDIS_USERNAME= REDIS_PASSWORD= -REDIS_TLS=false ``` +`REDIS_DB` e `REDIS_PASSWORD` são opcionais conforme a configuração do serviço. A conexão Redis usa TLS obrigatoriamente; se `REDIS_HOST` for `localhost`, o Redis local também precisa estar configurado para TLS. + Nunca versione senhas, tokens ou credenciais do Cloudinary. O arquivo `.env.example` deve conter somente valores de referência. ## 🚀 Execução local @@ -177,12 +176,11 @@ Essa resposta não representa a confirmação final do Cloudinary. O status fina ```http POST /anexos/ia -Authorization: Bearer -X-Acta-Usuario-Id: +Authorization: Bearer Content-Type: multipart/form-data ``` -A IA usa uma credencial própria de serviço e informa apenas o usuário em nome de quem está realizando o upload. A API valida se esse usuário está ativo, obtém a empresa diretamente de `public.usuario_sistema` e verifica se o ciclo pertence à mesma empresa. O `id_empresa` e o `firebase_uid` não são recebidos nessa rota. +A rota `/ia` usa a mesma autenticação Bearer de `POST /anexos`: a API consulta o usuário na `acta-pg-api` e utiliza o contexto retornado para verificar a empresa do ciclo. Não há token de serviço ou cabeçalho de usuário separado para essa rota. Os campos do formulário e a resposta `202 Accepted` são os mesmos de `POST /anexos`. @@ -195,14 +193,15 @@ GET /anexos/{id_anexo} Authorization: Bearer ``` -Para a IA: +Para a rota de integração da IA: ```http GET /anexos/ia/{id_anexo} -Authorization: Bearer -X-Acta-Usuario-Id: +Authorization: Bearer ``` +Essa rota usa a mesma autenticação Bearer de `GET /anexos/{id_anexo}`. + A resposta informa o status atual, se o envio terminou com sucesso e a URL gerada pelo Cloudinary: ```json @@ -281,9 +280,7 @@ Os testes usam dublês para banco, Redis e Cloudinary. Portanto, a suíte valida └── worker.py # Inicialização do worker RQ ``` - - ## 🤝 Links e autoria - [Repositório](https://github.com/AppActa/acta-import-api) · [Licença MIT](LICENSE) · `acta.institutojef@gmail.com` -- Contribuições: use *issues* e *pull requests*; há um [PULL_REQUEST_TEMPLATE.md](PULL_REQUEST_TEMPLATE.md). +- Contribuições: use *issues* e *pull requests*; há um [PULL_REQUEST_TEMPLATE.md](PULL_REQUEST_TEMPLATE.md). \ No newline at end of file