diff --git a/config/database/mysql.json b/config/database/mysql.json index d8c2251..a0d5bc3 100644 --- a/config/database/mysql.json +++ b/config/database/mysql.json @@ -5,6 +5,7 @@ "user": "${DB_USER}", "password": "${DB_PASSWORD}", "aeskey": "${AES_KEY}", + "storage": "${STORAGE}", "charset": "utf8mb4", "pool": { "pre_ping": true, diff --git a/packages/v1/administrativo/repositories/ato_principal/ato_principal_save_multiple_repository.py b/packages/v1/administrativo/repositories/ato_principal/ato_principal_save_multiple_repository.py index ac08592..cea4ad7 100644 --- a/packages/v1/administrativo/repositories/ato_principal/ato_principal_save_multiple_repository.py +++ b/packages/v1/administrativo/repositories/ato_principal/ato_principal_save_multiple_repository.py @@ -11,11 +11,54 @@ from packages.v1.administrativo.schemas.ato_principal_schema import ( AtoPrincipalSaveSchema, ) +# NOVAS IMPORTAÇÕES para lidar com Base64 e Arquivos +import base64 +import os +from pathlib import Path + # Configuração da Chave AES DB_SETTINGS = get_database_settings() AES_KEY = getattr(DB_SETTINGS, "aeskey", None) +# --- CONFIGURAÇÃO DE DIRETÓRIO DE UPLOAD --- +# **IMPORTANTE:** Configure este caminho conforme sua arquitetura! +# Exemplo: pasta 'storage' no diretório raiz do projeto +# Use 'os.environ.get' para facilitar a configuração em diferentes ambientes. +UPLOAD_DIR = Path(os.environ.get("STORAGE", "./storage")) + + +def _save_file_from_base64(base64_content: str, file_name: str) -> str: + """ + Decodifica o Base64, salva o arquivo no disco e retorna o caminho/URL do arquivo salvo. + """ + # Garante que o diretório de uploads existe + UPLOAD_DIR.mkdir(parents=True, exist_ok=True) + + # Define o caminho completo do arquivo + file_path = UPLOAD_DIR / file_name + + try: + # Decodificação Base64 + file_bytes = base64.b64decode(base64_content) + + # Escrita do arquivo em disco (modo binário 'wb') + with open(file_path, "wb") as f: + f.write(file_bytes) + + # Retorna o caminho onde o arquivo foi salvo (Este será o valor salvo no campo 'url' do banco) + return str(file_path) + + except base64.binascii.Error: + # Erro específico para conteúdo Base64 inválido + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"O conteúdo do arquivo '{file_name}' não é um Base64 válido.", + ) + except Exception as e: + # Outros erros de I/O ou sistema + raise Exception(f"Erro inesperado ao salvar arquivo '{file_name}': {e}") + class SaveMultipleRepository: """ @@ -41,6 +84,7 @@ class SaveMultipleRepository: exclude_unset=True, exclude={"ato_partes", "ato_documentos", "atos_vinculados"}, ) + # ... (restante do bloco de pré-processamento/criptografia do Ato Principal é mantido) # Define o ID de origem se for um ato vinculado if parent_ato_principal_id is not None: @@ -89,8 +133,6 @@ class SaveMultipleRepository: for campo in parte_campos_criptografar: valor = parte_data.get(campo) - # A validação/conversão para string já foi feita pelo Pydantic, - # mas mantemos a checagem de tipo e strip para segurança antes da criptografia if isinstance(valor, str) and valor.strip(): parte_data[campo] = func.aes_encrypt(valor, AES_KEY) else: @@ -99,10 +141,31 @@ class SaveMultipleRepository: new_parte = AtoParte(**parte_data, ato_principal_id=new_ato_id) db.add(new_parte) - # 4. Salva os Filhos Diretos: Ato Documentos + # 4. Salva os Filhos Diretos: Ato Documentos (BLOCO MODIFICADO) for doc in ato_schema.ato_documentos: - doc_data = doc.model_dump(exclude_unset=True) + # Pydantic v2 model_dump usa o argumento 'exclude' para excluir campos por nome + # Usamos 'exclude_unset' para evitar campos opcionais que não foram enviados + doc_data = doc.model_dump( + exclude_unset=True, + exclude={"arquivo_base64"}, # <<< Exclui o Base64 ANTES de ir pro banco + ) + # --- NOVO FLUXO DE ARQUIVOS (Base64 -> Disco) --- + # O campo 'arquivo_base64' foi excluído do doc_data acima. + # Agora verificamos se ele existe no objeto original (doc) + base64_content = getattr(doc, "arquivo_base64", None) + file_url_path = None + + if base64_content and doc.nome_documento: + # 1. Decodifica e salva o Base64 em disco + file_name = doc.nome_documento + file_url_path = _save_file_from_base64(base64_content, file_name) + + # 2. Atribui o caminho do arquivo salvo (ou None, se não foi enviado) ao campo 'url' do banco + doc_data["url"] = file_url_path + + # 3. Criptografia dos Metadados do Documento (url, nome, tipo) + # O campo 'url' agora contém o caminho do disco ou None. doc_campos_criptografar = ["url", "nome_documento", "tipo_documento"] for campo in doc_campos_criptografar: @@ -110,8 +173,11 @@ class SaveMultipleRepository: if isinstance(valor, str) and valor.strip(): doc_data[campo] = func.aes_encrypt(valor, AES_KEY) else: + # Garante que campos vazios ou None sejam inseridos como NULL doc_data[campo] = None + # 4. Criação e Persistência no Banco + # Note: doc_data NÃO tem mais 'arquivo_base64'. Ele tem 'url' (path do disco). new_documento = AtoDocumento(**doc_data, ato_principal_id=new_ato_id) db.add(new_documento) diff --git a/packages/v1/administrativo/schemas/ato_documento_schema.py b/packages/v1/administrativo/schemas/ato_documento_schema.py index d55a3fc..1de5357 100644 --- a/packages/v1/administrativo/schemas/ato_documento_schema.py +++ b/packages/v1/administrativo/schemas/ato_documento_schema.py @@ -3,11 +3,12 @@ from fastapi import HTTPException, status from typing import Optional from datetime import datetime +# Funções para sanitização de entradas (evitar XSS, SQLi etc.) +from actions.validations.text import Text + # Funções para sanitização de entradas (evitar XSS, SQLi etc.) # É importante que esta função seja mantida/implementada no seu ambiente # from actions.validations.text import Text # Descomentar se for usar -# Funções para validar URL (ajustar importação conforme seu projeto) -# from actions.validations.url import URL # Descomentar se for usar # ---------------------------------------------------- @@ -17,7 +18,7 @@ from datetime import datetime class AtoDocumentoSchema(BaseModel): ato_documento_id: Optional[int] = None ato_principal_id: Optional[int] = None # bigint NOT NULL - url: Optional[str] = None # text NOT NULL + arquivo_base64: Optional[str] = None nome_documento: Optional[str] = None # varchar(255) NOT NULL tipo_documento: Optional[str] = None # varchar(50) NOT NULL created_at: Optional[datetime] = None @@ -53,27 +54,11 @@ class AtoDocumentoIdSchema(BaseModel): # ---------------------------------------------------- class AtoDocumentoSaveSchema(BaseModel): # Campos obrigatórios - ato_principal_id: Optional[int] = None # <<< tornar opcional - url: str + ato_principal_id: Optional[int] = None + arquivo_base64: Optional[str] = None nome_documento: constr(max_length=255) tipo_documento: constr(max_length=50) - # Nota: created_at e updated_at são tratados pelo Model/Banco - - # Validação e Sanitização de URL (chk_url_https) - @field_validator("url") - def validate_url(cls, v: str): - v = v.strip() - if not v.startswith("https://"): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=[ - {"input": "url", "message": "A URL do documento deve ser HTTPS."} - ], - ) - # Adicionar aqui a validação de URL (ex: URL.is_valid_url(v)) se disponível - return v - # Validação e Sanitização de Tipo Documento (chk_tipo_documento_not_empty) @field_validator("tipo_documento") def validate_tipo_documento(cls, v: str): @@ -88,8 +73,9 @@ class AtoDocumentoSaveSchema(BaseModel): } ], ) - # Adicionar aqui a sanitização de texto (ex: Text.sanitize(v)) se disponível - return v + + # Sanitiza o campo + return Text.sanitize_input(v) # Validação e Sanitização de Nome Documento @field_validator("nome_documento") @@ -105,7 +91,9 @@ class AtoDocumentoSaveSchema(BaseModel): } ], ) - return v + + # Sanitiza o campo + return Text.sanitize_input(v) # ---------------------------------------------------- @@ -114,25 +102,10 @@ class AtoDocumentoSaveSchema(BaseModel): class AtoDocumentoUpdateSchema(BaseModel): # Todos os campos são opcionais no UPDATE ato_principal_id: Optional[int] = None - url: Optional[str] = None + arquivo_base64: Optional[str] = None nome_documento: Optional[constr(max_length=255)] = None tipo_documento: Optional[constr(max_length=50)] = None - # Validação de URL - @field_validator("url") - def validate_url(cls, v: Optional[str]): - if v is None: - return v - v = v.strip() - if v and not v.startswith("https://"): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=[ - {"input": "url", "message": "A URL do documento deve ser HTTPS."} - ], - ) - return v - # Validação de Tipo Documento @field_validator("tipo_documento") def validate_tipo_documento(cls, v: Optional[str]): @@ -149,4 +122,6 @@ class AtoDocumentoUpdateSchema(BaseModel): } ], ) - return v + + # Sanitiza o campo + return Text.sanitize_input(v) diff --git a/packages/v1/administrativo/schemas/ato_parte_schema.py b/packages/v1/administrativo/schemas/ato_parte_schema.py index 3312a84..dadd32e 100644 --- a/packages/v1/administrativo/schemas/ato_parte_schema.py +++ b/packages/v1/administrativo/schemas/ato_parte_schema.py @@ -4,6 +4,9 @@ from typing import Optional from datetime import datetime import re +# Funções para sanitização de entradas (evitar XSS, SQLi etc.) +from actions.validations.text import Text + # Funções para sanitização de entradas (evitar XSS, SQLi etc.) # É importante que esta função seja mantida/implementada no seu ambiente # from actions.validations.text import Text # Descomentar se for usar @@ -81,8 +84,8 @@ def validate_nome_not_empty(cls, v: str): } ], ) - # Adicionar aqui a sanitização de texto (ex: Text.sanitize(v)) se disponível - return v + # Sanitiza o campo + return Text.sanitize_input(v) # ---------------------------------------------------- @@ -102,12 +105,12 @@ class AtoParteSaveSchema(BaseModel): # Validação de Nome @field_validator("nome") def validate_nome(cls, v: str): - return validate_nome_not_empty(cls, v) + return validate_nome_not_empty(cls, Text.sanitize_input(v)) # Validação de CPF/CNPJ @field_validator("cpf_cnpj") def validate_cpf_cnpj_field(cls, v: str): - return validate_cpf_cnpj(cls, v) + return validate_cpf_cnpj(cls, Text.sanitize_input(v)) # ---------------------------------------------------- @@ -125,14 +128,14 @@ class AtoParteUpdateSchema(BaseModel): def validate_nome_update(cls, v: Optional[str]): if v is None: return v - return validate_nome_not_empty(cls, v) + return validate_nome_not_empty(cls, Text.sanitize_input(v)) # Validação de CPF/CNPJ @field_validator("cpf_cnpj") def validate_cpf_cnpj_update(cls, v: Optional[str]): if v is None: - return v - return validate_cpf_cnpj(cls, v) + return Text.sanitize_input(v) + return validate_cpf_cnpj(cls, Text.sanitize_input(v)) # Nota: Telefone não precisa de validação complexa além do constr(max_length) # se o objetivo for apenas armazenar a string fornecida. diff --git a/packages/v1/administrativo/schemas/ato_principal_schema.py b/packages/v1/administrativo/schemas/ato_principal_schema.py index ec6f741..e301486 100644 --- a/packages/v1/administrativo/schemas/ato_principal_schema.py +++ b/packages/v1/administrativo/schemas/ato_principal_schema.py @@ -5,6 +5,9 @@ from typing import Optional from datetime import datetime from decimal import Decimal # Importar Decimal para campos monetários +# Funções para sanitização de entradas (evitar XSS, SQLi etc.) +from actions.validations.text import Text + # Funções para sanitização de entradas (evitar XSS, SQLi etc.) # É importante que esta função seja mantida/implementada no seu ambiente # from actions.validations.text import Text # Descomentar se for usar @@ -124,7 +127,7 @@ class AtoPrincipalCodigoAtoSchema(BaseModel): ], ) - return v + return Text.sanitize_input(v) # ---------------------------------------------------- @@ -165,6 +168,7 @@ class AtoPrincipalSaveSchema(BaseModel): "codigo_ato", "nome_civil_ato", "nome_serventuario_praticou_ato", + "inteiro_teor", ) def validate_required_strings(cls, v: str): v = v.strip() @@ -183,7 +187,7 @@ class AtoPrincipalSaveSchema(BaseModel): ], ) # Adicionar aqui a sanitização de texto (ex: Text.sanitize(v)) se disponível - return v + return Text.sanitize_input(v) # Validação dos campos monetários (baseado na CHECK CONSTRAINT da DDL) @field_validator( @@ -206,7 +210,7 @@ class AtoPrincipalSaveSchema(BaseModel): } ], ) - return v + return Text.sanitize_input(v) class Config: from_attributes = True @@ -257,7 +261,7 @@ class AtoPrincipalUpdateSchema(BaseModel): } ], ) - return v + return Text.sanitize_input(v) # Reutiliza a validação de valores positivos @field_validator( @@ -274,7 +278,7 @@ class AtoPrincipalUpdateSchema(BaseModel): } ], ) - return v + return Text.sanitize_input(v) class Config: from_attributes = True