commit f353cf630f8b977b9b11ba78f6741af78cd8bbd9 Author: Keven Willian <52287668+kevenwillianps@users.noreply.github.com> Date: Mon Jun 30 08:56:33 2025 -0300 [BE-01] feat: Implementação de CRUD da tela de Caixa diff --git a/Api/.gitattributes b/Api/.gitattributes new file mode 100644 index 0000000..0e0f35c --- /dev/null +++ b/Api/.gitattributes @@ -0,0 +1,9 @@ +# Normaliza finais de linha +* text=auto + +# Força Python e arquivos de configuração a usarem LF +*.py text eol=lf +*.sh text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.env text eol=lf diff --git a/Api/.gitignore b/Api/.gitignore new file mode 100644 index 0000000..80ee3d1 --- /dev/null +++ b/Api/.gitignore @@ -0,0 +1,38 @@ +# Ambiente virtual +venv/ +.env +.env.* + +# Bytecode compilado +__pycache__/ +*.py[cod] +*$py.class + +# Arquivos temporários do sistema +.DS_Store +Thumbs.db + +# Logs e databases locais +*.log +*.sqlite3 + +# VSCode +.vscode/ + +# PyCharm +.idea/ + +# Arquivos de testes ou builds +*.coverage +htmlcov/ +coverage.xml +dist/ +build/ +.eggs/ +*.egg-info/ + +# Cache do pip +pip-wheel-metadata/ +*.egg +.cache/ +.tox/ \ No newline at end of file diff --git a/Api/Dockerfile b/Api/Dockerfile new file mode 100644 index 0000000..8f41d41 --- /dev/null +++ b/Api/Dockerfile @@ -0,0 +1,26 @@ +# Usa a imagem oficial do Python +FROM python:3.12-slim + +# Define diretório de trabalho no container +WORKDIR /app + +# Copia o arquivo de dependências +COPY requirements.txt . + +# Instala dependências no sistema e no Python +RUN apt-get update && apt-get install -y \ + gcc libffi-dev libssl-dev python3-dev firebird-dev \ + && pip install --upgrade pip \ + && pip install --no-cache-dir -r requirements.txt \ + && apt-get remove -y gcc \ + && apt-get autoremove -y \ + && rm -rf /var/lib/apt/lists/* + +# Copia o restante do projeto para o container +COPY . . + +# Expõe a porta padrão do Uvicorn/FastAPI +EXPOSE 8000 + +# Comando para iniciar o servidor +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/Api/FBCLIENT.DLL b/Api/FBCLIENT.DLL new file mode 100644 index 0000000..2cf2417 Binary files /dev/null and b/Api/FBCLIENT.DLL differ diff --git a/Api/README.md b/Api/README.md new file mode 100644 index 0000000..72060f6 --- /dev/null +++ b/Api/README.md @@ -0,0 +1,26 @@ +# 🧾 MyDocs - Gerador de Documentos + +Aplicação web para gerenciamento de empresas e geração automatizada de documentos com base em **minutas personalizadas** e **informações cadastradas**. + +## 🚀 Funcionalidades + +### ✅ Concluídas: +- **Autenticação de Usuário** + - Sistema de login seguro +- **CRUD de Empresas** + - Cadastro, edição, visualização e exclusão de empresas + +### 🛠️ Em Desenvolvimento: +- **Gerador de Documentos** + - Geração dinâmica de documentos com base em minutas e dados de empresas +- **Cadastro de Minutas** + - Sistema para criação e gerenciamento de templates/minutas de documentos +- **Cadastro de Produtos** + - Cadastro de produtos vinculados a empresas ou documentos + +## 🧩 Tecnologias Utilizadas + +- **Backend:** Python / FastApi +- **Frontend:** React / NextJs +- **Banco de Dados:** MySQL +- **Autenticação:** JWT \ No newline at end of file diff --git a/Api/api/v1/api.py b/Api/api/v1/api.py new file mode 100644 index 0000000..b87360c --- /dev/null +++ b/Api/api/v1/api.py @@ -0,0 +1,13 @@ +# Importa o gerenciador de rotas do FastAPI +from fastapi import APIRouter + +# Importa os módulos de rotas específicos +from api.v1.packages.administrative.endpoints import c_caixa_item + +# Cria uma instância do APIRouter que vai agregar todas as rotas da API +api_router = APIRouter() + +# Inclui as rotas de caixa +api_router.include_router( + c_caixa_item.router, prefix="/administrativo/caixa", tags=["Caixa"] +) diff --git a/Api/api/v1/packages/administrative/actions/c_caixa_item/delete_action.py b/Api/api/v1/packages/administrative/actions/c_caixa_item/delete_action.py new file mode 100644 index 0000000..530e475 --- /dev/null +++ b/Api/api/v1/packages/administrative/actions/c_caixa_item/delete_action.py @@ -0,0 +1,13 @@ +from core.base.base_action import BaseAction +from api.v1.packages.administrative.schemas.c_caixa_item_schema import CaixaItemSchema +from api.v1.packages.administrative.repositories.c_caixa_item.delete import Delete + +class DeleteAction(BaseAction): + + def execute(self, caixa_item_schema : CaixaItemSchema): + + # Instânciamento de repoistório + delete = Delete() + + # Retorna o resultado da operação + return delete.execute(caixa_item_schema) \ No newline at end of file diff --git a/Api/api/v1/packages/administrative/actions/c_caixa_item/index_action.py b/Api/api/v1/packages/administrative/actions/c_caixa_item/index_action.py new file mode 100644 index 0000000..8049c76 --- /dev/null +++ b/Api/api/v1/packages/administrative/actions/c_caixa_item/index_action.py @@ -0,0 +1,12 @@ +from core.base.base_action import BaseAction +from api.v1.packages.administrative.repositories.c_caixa_item.index import Index + +class IndexAction(BaseAction): + + def execute(self): + + # Instânciamento de repositório + index = Index() + + # Retorna todos produtos + return index.execute() \ No newline at end of file diff --git a/Api/api/v1/packages/administrative/actions/c_caixa_item/save_action.py b/Api/api/v1/packages/administrative/actions/c_caixa_item/save_action.py new file mode 100644 index 0000000..671b45c --- /dev/null +++ b/Api/api/v1/packages/administrative/actions/c_caixa_item/save_action.py @@ -0,0 +1,13 @@ +from api.v1.packages.administrative.schemas.c_caixa_item_schema import CaixaItemSchema +from api.v1.packages.administrative.repositories.c_caixa_item.create import Create +from core.base.base_action import BaseAction + +class SaveAction(BaseAction): + + def execute(self, caixa_item_schema : CaixaItemSchema): + + # Instância o repositório desejado + create = Create() + + # Executa o respositório desejado + return create.execute(caixa_item_schema) \ No newline at end of file diff --git a/Api/api/v1/packages/administrative/actions/c_caixa_item/show_action.py b/Api/api/v1/packages/administrative/actions/c_caixa_item/show_action.py new file mode 100644 index 0000000..3322aca --- /dev/null +++ b/Api/api/v1/packages/administrative/actions/c_caixa_item/show_action.py @@ -0,0 +1,13 @@ +from api.v1.packages.administrative.schemas.c_caixa_item_schema import CaixaItemSchema +from api.v1.packages.administrative.repositories.c_caixa_item.show import Show +from core.base.base_action import BaseAction + +class ShowAction(BaseAction): + + def execute(self, caixa_item_schema : CaixaItemSchema): + + # Instânciamento do repositório + show = Show() + + # Retorna os dados localizados + return show.execute(caixa_item_schema) \ No newline at end of file diff --git a/Api/api/v1/packages/administrative/controllers/c_caixa_item_controller.py b/Api/api/v1/packages/administrative/controllers/c_caixa_item_controller.py new file mode 100644 index 0000000..9bd5132 --- /dev/null +++ b/Api/api/v1/packages/administrative/controllers/c_caixa_item_controller.py @@ -0,0 +1,47 @@ +# Importação de bibliotecas +from core.utils.dynamic_import import DynamicImport +from api.v1.packages.administrative.schemas.c_caixa_item_schema import CaixaItemSchema + +class CCaixaItemController: + + def index(self): + + # Importação da classe desejad + indexService = DynamicImport.service("administrative", "c_caixa_item", "index_service", "IndexService") + + # Intânciamento da classe service + self.indexService = indexService() + + # Lista todos os produtos + return self.indexService.execute() + + def create(self, caixa_item_schema: CaixaItemSchema): + + # Importação da classe desejada + createService = DynamicImport.service("administrative", "c_caixa_item", "save_service", "SaveService") + + # Intânciamento da classe service + self.createService = createService() + + # Lista todos os produtos + return self.createService.execute(caixa_item_schema) + + def show(self, caixa_item_schema: CaixaItemSchema): + # Importação da classe desejad + showService = DynamicImport.service("administrative", "c_caixa_item", "show_service", "ShowService") + + # Intânciamento da classe service + self.showService = showService() + + # Lista todos os produtos + return self.showService.execute(caixa_item_schema) + + def delete(self, caixa_item_schema: CaixaItemSchema): + # Importação da classe desejad + deleteService = DynamicImport.service("administrative", "c_caixa_item", "delete_service", "DeleteService") + + # Intânciamento da classe service + self.deleteService = deleteService() + + # Lista todos os produtos + return self.deleteService.execute(caixa_item_schema) \ No newline at end of file diff --git a/Api/api/v1/packages/administrative/endpoints/c_caixa_item.py b/Api/api/v1/packages/administrative/endpoints/c_caixa_item.py new file mode 100644 index 0000000..7047a09 --- /dev/null +++ b/Api/api/v1/packages/administrative/endpoints/c_caixa_item.py @@ -0,0 +1,59 @@ +# Importação de bibliotecas +from fastapi import APIRouter, status, Depends +from api.v1.packages.administrative.controllers.c_caixa_item_controller import CCaixaItemController +from api.v1.packages.administrative.schemas.c_caixa_item_schema import CaixaItemSchema + +# Inicializar o roteaodr para as rotas de produtos +router = APIRouter() + +# Instãnciamento do controller desejado +cCaixaItemController = CCaixaItemController() + +@router.get("/", status_code=status.HTTP_200_OK) +async def index(): + # Busca todos os produtos cadastrados + response = cCaixaItemController.index() + + # Retornar os dados localizados + return { + "data": response + } + +@router.post('/', status_code=status.HTTP_201_CREATED) +async def save(caixa_item_schema: CaixaItemSchema): + + # Salva o produto desejado + response = cCaixaItemController.create(caixa_item_schema) + + # Retorna a informação desejada + return { + "data": response + } + +@router.get('/{caixa_item_id}', status_code=status.HTTP_200_OK) +async def show(caixa_item_id : int): + + # Armazena o produto id no Schema + CaixaItemSchema.caixa_item_id = caixa_item_id + + # Salva o produto desejado + response = cCaixaItemController.show(CaixaItemSchema) + + # Retorna a informação desejada + return { + "data": response + } + +@router.delete('/{caixa_item_id}', status_code=status.HTTP_200_OK) +async def delete(caixa_item_id : int): + + # Armazena o produto id no Schema + CaixaItemSchema.caixa_item_id = caixa_item_id + + # Salva o produto desejado + response = cCaixaItemController.delete(CaixaItemSchema) + + # Retorna a informação desejada + return { + "data": response + } diff --git a/Api/api/v1/packages/administrative/repositories/c_caixa_item/create.py b/Api/api/v1/packages/administrative/repositories/c_caixa_item/create.py new file mode 100644 index 0000000..ad9642a --- /dev/null +++ b/Api/api/v1/packages/administrative/repositories/c_caixa_item/create.py @@ -0,0 +1,29 @@ +# Importação de bibliotecas +from core.base.base_repository import BaseRepository +from api.v1.packages.administrative.schemas.c_caixa_item_schema import CaixaItemSchema + +class Create(BaseRepository): + + def execute(self, caixa_item : CaixaItemSchema): + + # Realiza a inserção do registro + self.cursor.execute( + """ + INSERT INTO C_CAIXA_ITEM ( + CAIXA_ITEM_ID, + CAIXA_SERVICO_ID + ) VALUES ( + ?, + ? + ); + """, + ( + caixa_item.caixa_item_id, caixa_item.caixa_servico_id, + ), + ) + + # Comita a transação + self.commit() + + # Retorna como verdadeiro se for salvo com sucesso + return True \ No newline at end of file diff --git a/Api/api/v1/packages/administrative/repositories/c_caixa_item/delete.py b/Api/api/v1/packages/administrative/repositories/c_caixa_item/delete.py new file mode 100644 index 0000000..fe07880 --- /dev/null +++ b/Api/api/v1/packages/administrative/repositories/c_caixa_item/delete.py @@ -0,0 +1,14 @@ +from core.base.base_repository import BaseRepository +from api.v1.packages.administrative.schemas.c_caixa_item_schema import CaixaItemSchema + +class Delete(BaseRepository): + + def execute(self, caixa_item : CaixaItemSchema): + + # Realiza a remoção + self.cursor.execute(""" DELETE FROM c_caixa_item cci WHERE cci.caixa_item_id = ?""", (caixa_item.caixa_item_id,)) + + # Comita a transação + self.commit() + + return True \ No newline at end of file diff --git a/Api/api/v1/packages/administrative/repositories/c_caixa_item/index.py b/Api/api/v1/packages/administrative/repositories/c_caixa_item/index.py new file mode 100644 index 0000000..b5faf53 --- /dev/null +++ b/Api/api/v1/packages/administrative/repositories/c_caixa_item/index.py @@ -0,0 +1,14 @@ +# Importação de bibliotecas +from core.base.base_repository import BaseRepository + +class Index(BaseRepository): + + def execute(self): + + self.cursor.execute("""SELECT * FROM c_caixa_item cci""") + columns = [col[0] for col in self.cursor.description] # lista dos nomes das colunas + results = [] + for row in self.cursor.fetchall(): + results.append({columns[i]: row[i] for i in range(len(columns))}) + self.commit() + return results diff --git a/Api/api/v1/packages/administrative/repositories/c_caixa_item/show.py b/Api/api/v1/packages/administrative/repositories/c_caixa_item/show.py new file mode 100644 index 0000000..54cd8ea --- /dev/null +++ b/Api/api/v1/packages/administrative/repositories/c_caixa_item/show.py @@ -0,0 +1,21 @@ +from core.base.base_repository import BaseRepository +from api.v1.packages.administrative.schemas.c_caixa_item_schema import CaixaItemSchema + +class Show(BaseRepository): + + def execute(self, caixa_item_schema: CaixaItemSchema): + + # Executa a busca pelo ID usando placeholder correto (?) + self.cursor.execute( + """SELECT * FROM c_caixa_item cci WHERE cci.caixa_item_id = ?""", + (caixa_item_schema.caixa_item_id,) # Importante: precisa ser tupla! + ) + + row = self.cursor.fetchone() + + if not row: + return None + + # Transforma em dict associativo + columns = [desc[0].lower() for desc in self.cursor.description] + return dict(zip(columns, row)) \ No newline at end of file diff --git a/Api/api/v1/packages/administrative/schemas/c_caixa_item_schema.py b/Api/api/v1/packages/administrative/schemas/c_caixa_item_schema.py new file mode 100644 index 0000000..0e6ccab --- /dev/null +++ b/Api/api/v1/packages/administrative/schemas/c_caixa_item_schema.py @@ -0,0 +1,52 @@ +from typing import Optional +from pydantic import BaseModel +from datetime import date, datetime + +class CaixaItemSchema(BaseModel): + especie_pagamento: Optional[str] = None + caixa_item_id: Optional[int] = None + caixa_servico_id: Optional[int] = None + usuario_servico_id: Optional[int] = None + usuario_caixa_id: Optional[int] = None + chave_servico: Optional[str] = None + descricao: Optional[str] = None + data_pagamento: Optional[date] = None + situacao: Optional[str] = None + tipo_documento: Optional[str] = None + tipo_transacao: Optional[str] = None + valor_servico: Optional[float] = None + valor_pago: Optional[float] = None + observacao: Optional[str] = None + caixa_cheque_id: Optional[int] = None + hora_pagamento: Optional[datetime] = None + caixa_id: Optional[int] = None + recibo_id: Optional[int] = None + tipo_servico: Optional[str] = None + qtd: Optional[int] = None + apresentante: Optional[str] = None + mensalista_id: Optional[int] = None + quitado_caixa_id: Optional[int] = None + registrado: Optional[bool] = None + emolumento: Optional[float] = None + taxa_judiciaria: Optional[float] = None + fundesp: Optional[float] = None + desconto: Optional[float] = None + valor_documento: Optional[float] = None + outra_taxa1: Optional[float] = None + chave_servico_sec: Optional[str] = None + emolumento_item_id: Optional[int] = None + caixa_registroselo_id: Optional[int] = None + fundo_ri: Optional[float] = None + valor_recibo: Optional[float] = None + boleto_pdf: Optional[str] = None # ou `bytes` se for binário + boleto_vencimento: Optional[date] = None + iss: Optional[float] = None + nlote: Optional[int] = None + tabela: Optional[str] = None + campo_id: Optional[int] = None + boleto_id: Optional[int] = None + valor_adicional: Optional[float] = None + pix_id: Optional[int] = None + + class Config: + from_attributes = True diff --git a/Api/api/v1/packages/administrative/services/c_caixa_item/go/delete_service.py b/Api/api/v1/packages/administrative/services/c_caixa_item/go/delete_service.py new file mode 100644 index 0000000..267a28b --- /dev/null +++ b/Api/api/v1/packages/administrative/services/c_caixa_item/go/delete_service.py @@ -0,0 +1,28 @@ +from fastapi import HTTPException, status +from api.v1.packages.administrative.actions.c_caixa_item.delete_action import DeleteAction +from api.v1.packages.administrative.actions.c_caixa_item.show_action import ShowAction +from api.v1.packages.administrative.schemas.c_caixa_item_schema import CaixaItemSchema + + +class DeleteService: + + def execute(self, caixa_item_schema: CaixaItemSchema): + + # Instânciamento de classe + showAction = ShowAction() + + # Obtem o registro desejado + data = showAction.execute(caixa_item_schema) + + # Verifica se o registro não existe + if not data: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='Não foi possível localizar o registro' + ) + + # Instânciamento de ações + deleteAction = DeleteAction() + + # Retorna todos produtos desejados + return deleteAction.execute(caixa_item_schema) \ No newline at end of file diff --git a/Api/api/v1/packages/administrative/services/c_caixa_item/go/index_service.py b/Api/api/v1/packages/administrative/services/c_caixa_item/go/index_service.py new file mode 100644 index 0000000..80ed6cc --- /dev/null +++ b/Api/api/v1/packages/administrative/services/c_caixa_item/go/index_service.py @@ -0,0 +1,11 @@ +from api.v1.packages.administrative.actions.c_caixa_item.index_action import IndexAction + +class IndexService: + + def execute(self): + + # Instânciamento de ações + indexAction = IndexAction() + + # Retorna todos produtos desejados + return indexAction.execute() \ No newline at end of file diff --git a/Api/api/v1/packages/administrative/services/c_caixa_item/go/save_service.py b/Api/api/v1/packages/administrative/services/c_caixa_item/go/save_service.py new file mode 100644 index 0000000..65e6184 --- /dev/null +++ b/Api/api/v1/packages/administrative/services/c_caixa_item/go/save_service.py @@ -0,0 +1,13 @@ +from api.v1.packages.administrative.actions.c_caixa_item.save_action import SaveAction +from api.v1.packages.administrative.schemas.c_caixa_item_schema import CaixaItemSchema + + +class SaveService: + + def execute(self, caixa_item_schema: CaixaItemSchema): + + # Instânciamento de ações + saveAction = SaveAction() + + # Retorna todos produtos desejados + return saveAction.execute(caixa_item_schema) \ No newline at end of file diff --git a/Api/api/v1/packages/administrative/services/c_caixa_item/go/show_service.py b/Api/api/v1/packages/administrative/services/c_caixa_item/go/show_service.py new file mode 100644 index 0000000..5961b27 --- /dev/null +++ b/Api/api/v1/packages/administrative/services/c_caixa_item/go/show_service.py @@ -0,0 +1,25 @@ +from fastapi import HTTPException, status +from api.v1.packages.administrative.actions.c_caixa_item.show_action import ShowAction +from api.v1.packages.administrative.schemas.c_caixa_item_schema import CaixaItemSchema + + +class ShowService: + + def execute(self, caixa_item_schema: CaixaItemSchema): + + # Instânciamento de ações + showAction = ShowAction() + + # Retorna todos produtos desejados + data = showAction.execute(caixa_item_schema) + + # Verifica se foi localizado o registro + if not data: + # Retorna uma exceção + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='Não foi possível localizar o registro' + ) + + # Retorna a informação localizada + return data diff --git a/Api/config/database.json b/Api/config/database.json new file mode 100644 index 0000000..c0570a7 --- /dev/null +++ b/Api/config/database.json @@ -0,0 +1,10 @@ +{ + "firebird": { + "host": "localhost", + "name": "C:\\Users\\keven\\OneDrive\\Desktop\\Orius\\CALCILANDIA.FDB", + "port": 3050, + "user": "SYSDBA", + "password": "masterkey", + "charset": "UTF8" + } +} \ No newline at end of file diff --git a/Api/core/auth.py b/Api/core/auth.py new file mode 100644 index 0000000..9ed6818 --- /dev/null +++ b/Api/core/auth.py @@ -0,0 +1,34 @@ +from pytz import timezone +from datetime import datetime, timedelta + +from fastapi.security import OAuth2PasswordBearer +from jose import jwt + +from core.configs import settings + +# Define o esquema OAuth2 para login +oauth2_schema = OAuth2PasswordBearer( + tokenUrl=f"{settings.API_V1_STR}/usuarios/login" +) + +# Função para criar um token JWT +def create_token(tipo_token: str, tempo_vida: timedelta, sub: str) -> str: + payload = {} + sp = timezone('America/Sao_Paulo') + expira = datetime.now(tz=sp) + tempo_vida + + payload["type"] = tipo_token + payload["exp"] = expira + payload["iat"] = datetime.now(tz=sp) + payload["sub"] = str(sub) + + return jwt.encode(payload, settings.JWT_SECRET, algorithm=settings.ALGORITHM) + + +# Criação do token de acesso (access_token) +def create_access_token(sub: str) -> str: + return create_token( + tipo_token='access_token', + tempo_vida=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES), + sub=sub + ) diff --git a/Api/core/base/base_action.py b/Api/core/base/base_action.py new file mode 100644 index 0000000..481e5d5 --- /dev/null +++ b/Api/core/base/base_action.py @@ -0,0 +1,16 @@ +from abc import ABC, abstractmethod + +class BaseAction: + + """ + Classe abstrata base para todos as actions do sistema. + Obriga implementação de um método execute(). + """ + + @abstractmethod + def execute(self, *args, **kwargs): + """ + Método abstrato obrigatório a ser implementado pelas subclasses. + Deve conter a lógica principal do repositório. + """ + pass \ No newline at end of file diff --git a/Api/core/base/base_repository.py b/Api/core/base/base_repository.py new file mode 100644 index 0000000..3bcb824 --- /dev/null +++ b/Api/core/base/base_repository.py @@ -0,0 +1,43 @@ +from abc import ABC, abstractmethod +from core.database import get_connection + +class BaseRepository(ABC): + """ + Classe abstrata base para todos os repositórios do sistema. + Fornece conexão com o banco de dados e obriga implementação de um método execute(). + """ + + def __init__(self): + """ + Inicializa a conexão e o cursor do banco de dados. + Essa conexão deve ser usada pelas subclasses. + """ + self.conn = get_connection() + self.cursor = self.conn.cursor() + + @abstractmethod + def execute(self, *args, **kwargs): + """ + Método abstrato obrigatório a ser implementado pelas subclasses. + Deve conter a lógica principal do repositório. + """ + pass + + def commit(self): + """ + Realiza o commit da transação. + """ + self.conn.commit() + + def rollback(self): + """ + Realiza o rollback da transação. + """ + self.conn.rollback() + + def close(self): + """ + Fecha cursor e conexão. + """ + self.cursor.close() + self.conn.close() diff --git a/Api/core/configs.py b/Api/core/configs.py new file mode 100644 index 0000000..10d878b --- /dev/null +++ b/Api/core/configs.py @@ -0,0 +1,26 @@ +from pydantic_settings import BaseSettings + +# Classe de configurações globais da aplicação +class Settings(BaseSettings): + # Prefixo base das rotas da API + API_V1_STR: str = '/api/v1' + + # URL de conexão com o banco MySQL + # Formato: mysql://usuario:senha@host:porta/banco + DB_URL: str = "mysql://root:root@127.0.0.1:3306/mydocs" + + # Chave secreta para geração dos tokens JWT + JWT_SECRET: str = 'WYe1zwtlDkh39_X3X3qTSICFDxts4VQrMyGLxnEpGUg' + + # Algoritmo usado para assinar o token + ALGORITHM: str = 'HS256' + + # Tempo de expiração do token JWT (em minutos): 1 semana + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 + + # Configuração interna do Pydantic + class Config: + case_sensitive = True # Respeita letras maiúsculas/minúsculas nas variáveis de ambiente + +# Instância única das configurações +settings: Settings = Settings() diff --git a/Api/core/database.py b/Api/core/database.py new file mode 100644 index 0000000..216c4f3 --- /dev/null +++ b/Api/core/database.py @@ -0,0 +1,22 @@ +import fdb +from core.utils.config import Config + +def get_connection(): + """ + Constrói e retorna uma conexão com o banco MySQL + utilizando os dados da URL definida nas configurações. + """ + + # Obtem as configurações de banco de dados + database = Config.get() + + # Constrói o DSN no formato 'hostname/port:database_path' + # E essa string é passada como o PRIMEIRO ARGUMENTO POSICIONAL + connection_dsn = f"{database.firebird.host}/{database.firebird.port}:{database.firebird.name}" + + return fdb.connect( + connection_dsn, # Este é o DSN completo que o driver espera + user=database.firebird.user, + password=database.firebird.password, + charset=database.firebird.charset + ) diff --git a/Api/core/deps.py b/Api/core/deps.py new file mode 100644 index 0000000..70afd48 --- /dev/null +++ b/Api/core/deps.py @@ -0,0 +1,64 @@ +# core/deps.py + +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from jose import jwt, JWTError +from core.configs import settings +from api.v1.packages.users.models.users.users_model import UserModel # <--- Importe o UserModel + +# Define o esquema de segurança OAuth2 (token tipo Bearer) +oauth2_schema = OAuth2PasswordBearer( + tokenUrl=f"{settings.API_V1_STR}/usuarios/login" +) + +# Função que retorna o usuário autenticado com base no token JWT +def get_current_user(token: str = Depends(oauth2_schema)) -> dict: + credential_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Could not validate credentials', + headers={"WWW-Authenticate": "Bearer"}, + ) + + try: + payload = jwt.decode( + token, + settings.JWT_SECRET, + algorithms=[settings.ALGORITHM], + options={"verify_aud": False} + ) + + user_id: str = payload.get("sub") + + if user_id is None: + raise credential_exception + + except JWTError: + raise credential_exception + + # --- NOVO: Buscar os dados completos do usuário do banco de dados --- + # Convert user_id para int, se ele for um string no JWT e int no banco + try: + user_id_int = int(user_id) + except ValueError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid user ID format in token." + ) + + # Use o UserModel para buscar os dados completos + # Adicione um try-except para a chamada ao modelo para capturar erros de DB + try: + user = UserModel.get_by_id(user_id_int) + except Exception as e: # Captura qualquer erro ao buscar no DB + print(f"Error fetching user in get_current_user: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve user data. {str(e)}" + ) + + if not user: + # Se o usuário não for encontrado no DB (mas o token era válido para um ID), + # pode indicar um usuário deletado ou um ID inválido no token. + raise credential_exception # Ou HTTPException(404, "User associated with token not found") + + return user # Retorna o dicionário completo do usuário \ No newline at end of file diff --git a/Api/core/security.py b/Api/core/security.py new file mode 100644 index 0000000..c9cdda8 --- /dev/null +++ b/Api/core/security.py @@ -0,0 +1,31 @@ +# core/security.py + +from passlib.context import CryptContext # Contexto de criptografia de senhas +from passlib.exc import UnknownHashError # Exceção para hashes não reconhecidos + +# Define contexto de criptografia com esquema bcrypt +CRYPTO = CryptContext(schemes=['bcrypt'], deprecated='auto') + +def verify_senha_api(plain_senha_api: str, hashed_senha_api: str) -> bool: + """ + Compara a senha fornecida em texto puro com o hash armazenado. + Retorna False em caso de erro ou formato inválido. + """ + try: + if not plain_senha_api or not hashed_senha_api: + return False # Garante que nenhum dos valores seja nulo ou vazio + + # Verifica se a senha corresponde ao hash + return CRYPTO.verify(plain_senha_api, hashed_senha_api) + + except UnknownHashError: + return False # Hash inválido ou não reconhecido pelo passlib + + except Exception: + return False # Falha genérica na verificação + +def hash_senha_api(plain_senha_api: str) -> str: + """ + Gera o hash da senha em texto puro. + """ + return CRYPTO.hash(plain_senha_api) # Retorna a senha criptografada com bcrypt diff --git a/Api/core/system/exceptions.py b/Api/core/system/exceptions.py new file mode 100644 index 0000000..ec151c9 --- /dev/null +++ b/Api/core/system/exceptions.py @@ -0,0 +1,4 @@ +# exceptions.py +class BusinessRuleException(Exception): + def __init__(self, message: str): + self.message = message \ No newline at end of file diff --git a/Api/core/system/handlers.py b/Api/core/system/handlers.py new file mode 100644 index 0000000..d654b1d --- /dev/null +++ b/Api/core/system/handlers.py @@ -0,0 +1,42 @@ +# handlers.py +import traceback +from fastapi import Request +from fastapi.responses import JSONResponse +from fastapi.exceptions import RequestValidationError +from starlette.exceptions import HTTPException as StarletteHTTPException +from core.system.exceptions import BusinessRuleException + +def register_exception_handlers(app): + + @app.exception_handler(BusinessRuleException) + async def business_rule_exception_handler(request: Request, exc: BusinessRuleException): + return JSONResponse( + status_code=422, + content={"error": "Regra de negócio", "detail": exc.message} + ) + + @app.exception_handler(StarletteHTTPException) + async def http_exception_handler(request: Request, exc: StarletteHTTPException): + return JSONResponse( + status_code=exc.status_code, + content={"error": "HTTP Error", "detail": exc.detail} + ) + + @app.exception_handler(RequestValidationError) + async def validation_exception_handler(request: Request, exc: RequestValidationError): + return JSONResponse( + status_code=400, + content={"error": "Erro de validação", "detail": exc.errors()} + ) + + @app.exception_handler(Exception) + async def global_exception_handler(request: Request, exc: Exception): + return JSONResponse( + status_code=500, + content={ + "error": "Erro Interno do Servidor", + "type": type(exc).__name__, + "message": str(exc), + "trace": traceback.format_exc() + } + ) diff --git a/Api/core/utils/cep.py b/Api/core/utils/cep.py new file mode 100644 index 0000000..025eefc --- /dev/null +++ b/Api/core/utils/cep.py @@ -0,0 +1,8 @@ + +class CEP: + + @staticmethod + def validate(data: str) -> bool: + + # Valida e retorna a informação + return len(data) == 8 \ No newline at end of file diff --git a/Api/core/utils/cnpj.py b/Api/core/utils/cnpj.py new file mode 100644 index 0000000..cab46c9 --- /dev/null +++ b/Api/core/utils/cnpj.py @@ -0,0 +1,34 @@ +import re + +class CNPJ: + + @staticmethod + def validate(data: str) -> bool: + + # Remove caracteres não numéricos + data = re.sub(r'\D', '', data) + + # Verifica se tem 14 dígitos + if len(data) != 14: + return False + + # CNPJs com todos os dígitos iguais são inválidos + if data == data[0] * 14: + return False + + # Calcula os dois dígitos verificadores + def calcular_digito(data, peso): + soma = sum(int(a) * b for a, b in zip(data, peso)) + resto = soma % 11 + return '0' if resto < 2 else str(11 - resto) + + # Primeiro dígito verificador + peso1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2] + digito1 = calcular_digito(data[:12], peso1) + + # Segundo dígito verificador + peso2 = [6] + peso1 + digito2 = calcular_digito(data[:12] + digito1, peso2) + + # Verifica se os dígitos batem + return data[-2:] == digito1 + digito2 diff --git a/Api/core/utils/config.py b/Api/core/utils/config.py new file mode 100644 index 0000000..6bbe0eb --- /dev/null +++ b/Api/core/utils/config.py @@ -0,0 +1,20 @@ +import json +from types import SimpleNamespace +from pathlib import Path + + +class Config: + + @staticmethod + def get(): + # Caminho absoluto do arquivo atual + base_dir = Path(__file__).resolve().parent + + # Caminho absoluto para o config.json (subindo dois níveis e entrando em config/) + config_path = base_dir.parent.parent / 'config' / 'database.json' + + # Carrega o JSON como objeto acessível por ponto + with open(config_path, 'r') as f: + config = json.load(f, object_hook=lambda d: SimpleNamespace(**d)) + + return config \ No newline at end of file diff --git a/Api/core/utils/dynamic_import.py b/Api/core/utils/dynamic_import.py new file mode 100644 index 0000000..a098c85 --- /dev/null +++ b/Api/core/utils/dynamic_import.py @@ -0,0 +1,17 @@ +import importlib + +class DynamicImport: + + state = "go" + base = "api.v1.packages" + + @staticmethod + def service(package: str, table: str, name: str, class_name : str): + try: + module_file = f"{name}" + path = f"{DynamicImport.base}.{package}.services.{table}.{DynamicImport.state}.{module_file}" + module = importlib.import_module(path) + clazz = getattr(module, class_name) + return clazz + except (ImportError, AttributeError) as e: + raise ImportError(f"Erro ao importar '{class_name}' de '{path}': {e}") diff --git a/Api/core/utils/email.py b/Api/core/utils/email.py new file mode 100644 index 0000000..f884ee0 --- /dev/null +++ b/Api/core/utils/email.py @@ -0,0 +1,10 @@ +import re + +class Email: + + @staticmethod + def validate(data: str) -> str: + # Validação de email + default = r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$' + if not re.match(default, data): + raise f"Email inválido: {data}" \ No newline at end of file diff --git a/Api/core/utils/phone.py b/Api/core/utils/phone.py new file mode 100644 index 0000000..b509c7f --- /dev/null +++ b/Api/core/utils/phone.py @@ -0,0 +1,12 @@ + +class Phone: + + @staticmethod + def validate_cellphone(data: str) -> bool: + # Verifica e retorna se o numero de celular é igual a 11 + return len(data) == 11 + + @staticmethod + def validate_telephone(data: str) -> bool: + # Verifica e retorna se o numero de telefone é igual a 11 + return len(data) == 10 \ No newline at end of file diff --git a/Api/core/utils/text.py b/Api/core/utils/text.py new file mode 100644 index 0000000..1aee9fe --- /dev/null +++ b/Api/core/utils/text.py @@ -0,0 +1,18 @@ +import re +import html + +class Text: + + @staticmethod + def sanitize(data: str) -> str: + """Trim spaces, escape HTML entities, collapse multiple spaces.""" + data = data.strip() + data = html.escape(data) + data = re.sub(r"\s+", " ", data) + return data + + @staticmethod + def just_numbers(data: str) -> str: + """ Mantêm apenas os numeros """ + data = re.sub(r"[^\d]", "", data) + return data \ No newline at end of file diff --git a/Api/core/validation.py b/Api/core/validation.py new file mode 100644 index 0000000..61ac808 --- /dev/null +++ b/Api/core/validation.py @@ -0,0 +1,31 @@ +# utils/validation.py + +import re +import html + +class InputSanitizer: + + @staticmethod + def clean_text(text: str) -> str: + """Trim spaces, escape HTML entities, collapse multiple spaces.""" + text = text.strip() + text = html.escape(text) + text = re.sub(r"\s+", " ", text) + return text + + @staticmethod + def is_valid_email(email: str) -> bool: + """Check if email has a valid structure""" + return bool(re.match(r"^[\w\.-]+@[\w\.-]+\.\w+$", email)) + + @staticmethod + def has_script(text: str) -> bool: + """Detect basic XSS attempts""" + return " bool: + """Detect common XSS/SQL injection characters or patterns""" + blacklist = ["