From 9c4d32a65ccdf8eaa9694a511c3815fb81cd0e0e Mon Sep 17 00:00:00 2001 From: Kenio de Souza Date: Mon, 6 Oct 2025 09:30:41 -0300 Subject: [PATCH] first commit --- .gitattributes | 9 + .gitignore | 46 ++++ Dockerfile | 26 +++ abstracts/action.py | 17 ++ abstracts/repository.py | 100 +++++++++ actions/config/config.py | 20 ++ actions/dynamic_import/dynamic_import.py | 27 +++ actions/file/file.py | 32 +++ actions/jwt/create_token.py | 36 ++++ actions/jwt/get_current_user.py | 24 +++ actions/jwt/verify_token.py | 57 +++++ actions/log/log.py | 32 +++ actions/security/security.py | 43 ++++ actions/system/exceptions.py | 4 + actions/system/handlers.py | 86 ++++++++ actions/validations/cep.py | 8 + actions/validations/cnpj.py | 35 +++ actions/validations/cpf.py | 34 +++ actions/validations/email.py | 9 + actions/validations/phone.py | 12 ++ actions/validations/text.py | 63 ++++++ config/app.json | 28 +++ config/database/firebird_exemple.json | 13 ++ config/database/mysql.json | 13 ++ database/mysql.py | 73 +++++++ main.py | 89 ++++++++ packages/__init__.py | 0 .../actions/log/log_delete_action.py | 27 +++ .../actions/log/log_get_by_log_id_action.py | 26 +++ .../actions/log/log_index_action.py | 28 +++ .../actions/log/log_save_action.py | 26 +++ .../actions/log/log_show_action.py | 30 +++ .../actions/log/log_update_action.py | 27 +++ .../actions/user/user_delete_action.py | 11 + .../user/user_get_by_authenticate_action.py | 14 ++ .../actions/user/user_get_by_email_action.py | 13 ++ .../user/user_get_by_user_id_action.py | 12 ++ .../actions/user/user_index_action.py | 15 ++ .../actions/user/user_save_action.py | 11 + .../actions/user/user_show_action.py | 16 ++ .../actions/user/user_update_action.py | 11 + .../controllers/log_controller.py | 177 +++++++++++++++ .../controllers/user_controller.py | 145 +++++++++++++ .../administrativo/endpoints/log_endpoint.py | 111 ++++++++++ .../administrativo/endpoints/user_endpoint.py | 133 ++++++++++++ .../repositories/log/log_delete_repository.py | 38 ++++ .../log/log_get_by_log_id_repository.py | 30 +++ .../repositories/log/log_index_repository.py | 46 ++++ .../repositories/log/log_save_repository.py | 64 ++++++ .../repositories/log/log_show_repository.py | 29 +++ .../repositories/log/log_update_repository.py | 63 ++++++ .../user/user_delete_repository.py | 34 +++ .../user_get_by_authenticate_repository.py | 23 ++ .../user/user_get_by_email_repository.py | 18 ++ .../user/user_get_by_user_id_repository.py | 18 ++ .../user/user_index_repository.py | 14 ++ .../repositories/user/user_save_repository.py | 82 +++++++ .../repositories/user/user_show_repository.py | 17 ++ .../user/user_update_repository.py | 105 +++++++++ .../v1/administrativo/schemas/log_schema.py | 148 +++++++++++++ .../v1/administrativo/schemas/user_schema.py | 201 ++++++++++++++++++ .../services/log/log_delete_service.py | 19 ++ .../services/log/log_index_service.py | 25 +++ .../services/log/log_save_service.py | 44 ++++ .../services/log/log_show_service.py | 29 +++ .../services/log/log_update_service.py | 16 ++ .../user/user_authenticate_service.py | 52 +++++ .../services/user/user_delete_service.py | 15 ++ .../services/user/user_get_cpf_service.py | 26 +++ .../services/user/user_get_email_service.py | 26 +++ .../services/user/user_get_login_service.py | 26 +++ .../services/user/user_index_service.py | 24 +++ .../services/user/user_me_service.py | 18 ++ .../services/user/user_save_service.py | 49 +++++ .../services/user/user_show_service.py | 24 +++ .../services/user/user_update_service.py | 12 ++ packages/v1/api.py | 20 ++ .../connections/firebird_check_action.py | 22 ++ .../v1/system/actions/disk/get_size_action.py | 24 +++ .../system/service/startup_check_service.py | 12 ++ requirements.txt | 29 +++ server.bat | 6 + 82 files changed, 3217 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 abstracts/action.py create mode 100644 abstracts/repository.py create mode 100644 actions/config/config.py create mode 100644 actions/dynamic_import/dynamic_import.py create mode 100644 actions/file/file.py create mode 100644 actions/jwt/create_token.py create mode 100644 actions/jwt/get_current_user.py create mode 100644 actions/jwt/verify_token.py create mode 100644 actions/log/log.py create mode 100644 actions/security/security.py create mode 100644 actions/system/exceptions.py create mode 100644 actions/system/handlers.py create mode 100644 actions/validations/cep.py create mode 100644 actions/validations/cnpj.py create mode 100644 actions/validations/cpf.py create mode 100644 actions/validations/email.py create mode 100644 actions/validations/phone.py create mode 100644 actions/validations/text.py create mode 100644 config/app.json create mode 100644 config/database/firebird_exemple.json create mode 100644 config/database/mysql.json create mode 100644 database/mysql.py create mode 100644 main.py create mode 100644 packages/__init__.py create mode 100644 packages/v1/administrativo/actions/log/log_delete_action.py create mode 100644 packages/v1/administrativo/actions/log/log_get_by_log_id_action.py create mode 100644 packages/v1/administrativo/actions/log/log_index_action.py create mode 100644 packages/v1/administrativo/actions/log/log_save_action.py create mode 100644 packages/v1/administrativo/actions/log/log_show_action.py create mode 100644 packages/v1/administrativo/actions/log/log_update_action.py create mode 100644 packages/v1/administrativo/actions/user/user_delete_action.py create mode 100644 packages/v1/administrativo/actions/user/user_get_by_authenticate_action.py create mode 100644 packages/v1/administrativo/actions/user/user_get_by_email_action.py create mode 100644 packages/v1/administrativo/actions/user/user_get_by_user_id_action.py create mode 100644 packages/v1/administrativo/actions/user/user_index_action.py create mode 100644 packages/v1/administrativo/actions/user/user_save_action.py create mode 100644 packages/v1/administrativo/actions/user/user_show_action.py create mode 100644 packages/v1/administrativo/actions/user/user_update_action.py create mode 100644 packages/v1/administrativo/controllers/log_controller.py create mode 100644 packages/v1/administrativo/controllers/user_controller.py create mode 100644 packages/v1/administrativo/endpoints/log_endpoint.py create mode 100644 packages/v1/administrativo/endpoints/user_endpoint.py create mode 100644 packages/v1/administrativo/repositories/log/log_delete_repository.py create mode 100644 packages/v1/administrativo/repositories/log/log_get_by_log_id_repository.py create mode 100644 packages/v1/administrativo/repositories/log/log_index_repository.py create mode 100644 packages/v1/administrativo/repositories/log/log_save_repository.py create mode 100644 packages/v1/administrativo/repositories/log/log_show_repository.py create mode 100644 packages/v1/administrativo/repositories/log/log_update_repository.py create mode 100644 packages/v1/administrativo/repositories/user/user_delete_repository.py create mode 100644 packages/v1/administrativo/repositories/user/user_get_by_authenticate_repository.py create mode 100644 packages/v1/administrativo/repositories/user/user_get_by_email_repository.py create mode 100644 packages/v1/administrativo/repositories/user/user_get_by_user_id_repository.py create mode 100644 packages/v1/administrativo/repositories/user/user_index_repository.py create mode 100644 packages/v1/administrativo/repositories/user/user_save_repository.py create mode 100644 packages/v1/administrativo/repositories/user/user_show_repository.py create mode 100644 packages/v1/administrativo/repositories/user/user_update_repository.py create mode 100644 packages/v1/administrativo/schemas/log_schema.py create mode 100644 packages/v1/administrativo/schemas/user_schema.py create mode 100644 packages/v1/administrativo/services/log/log_delete_service.py create mode 100644 packages/v1/administrativo/services/log/log_index_service.py create mode 100644 packages/v1/administrativo/services/log/log_save_service.py create mode 100644 packages/v1/administrativo/services/log/log_show_service.py create mode 100644 packages/v1/administrativo/services/log/log_update_service.py create mode 100644 packages/v1/administrativo/services/user/user_authenticate_service.py create mode 100644 packages/v1/administrativo/services/user/user_delete_service.py create mode 100644 packages/v1/administrativo/services/user/user_get_cpf_service.py create mode 100644 packages/v1/administrativo/services/user/user_get_email_service.py create mode 100644 packages/v1/administrativo/services/user/user_get_login_service.py create mode 100644 packages/v1/administrativo/services/user/user_index_service.py create mode 100644 packages/v1/administrativo/services/user/user_me_service.py create mode 100644 packages/v1/administrativo/services/user/user_save_service.py create mode 100644 packages/v1/administrativo/services/user/user_show_service.py create mode 100644 packages/v1/administrativo/services/user/user_update_service.py create mode 100644 packages/v1/api.py create mode 100644 packages/v1/system/actions/connections/firebird_check_action.py create mode 100644 packages/v1/system/actions/disk/get_size_action.py create mode 100644 packages/v1/system/service/startup_check_service.py create mode 100644 requirements.txt create mode 100644 server.bat diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0e0f35c --- /dev/null +++ b/.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/.gitignore b/.gitignore new file mode 100644 index 0000000..05291f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# 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/ + +# Arquivo s de conexão +config/database/firebird.json +storage/temp +storage/temp.json + +# Ignorar arquivos storage +storage/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8826883 --- /dev/null +++ b/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 ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port 8000"] diff --git a/abstracts/action.py b/abstracts/action.py new file mode 100644 index 0000000..da31d8a --- /dev/null +++ b/abstracts/action.py @@ -0,0 +1,17 @@ +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/abstracts/repository.py b/abstracts/repository.py new file mode 100644 index 0000000..d979114 --- /dev/null +++ b/abstracts/repository.py @@ -0,0 +1,100 @@ +from typing import Optional, Literal +from sqlalchemy import text +from sqlalchemy.exc import SQLAlchemyError +from database.mysql import MySQL # Importa a classe MySQL que gerencia a engine + +class BaseRepository: + """ + Classe base para todos os repositórios. + Contém métodos genéricos para executar SQL no MySQL usando SQLAlchemy. + """ + + def query(self, sql: str, params: Optional[dict] = None): + """ + Executa uma query e retorna o ResultProxy bruto. + Útil para operações customizadas que precisam do objeto SQLAlchemy diretamente. + """ + return self._execute(sql, params, fetch="result") + + def fetch_all(self, sql: str, params: Optional[dict] = None): + """ + Executa uma query SQL e retorna todos os registros como lista de dicionários. + Retorna lista vazia se não encontrar nada. + """ + return self._execute(sql, params, fetch="all") + + def fetch_one(self, sql: str, params: Optional[dict] = None): + """ + Executa uma query SQL e retorna o primeiro registro como dicionário. + Retorna None se não encontrar nenhum registro. + """ + return self._execute(sql, params, fetch="one") + + def run(self, sql: str, params: Optional[dict] = None): + """ + Executa um SQL sem retorno (ex: INSERT, UPDATE, DELETE). + Não retorna nenhum dado. + """ + return self._execute(sql, params, fetch="none") + + def run_and_return(self, sql: str, params: Optional[dict] = None): + """ + Executa um INSERT e retorna o último ID gerado no MySQL. + Se for um SELECT, retorna o primeiro registro normalmente. + """ + engine = MySQL.get_engine() # Obtém a engine do MySQL + try: + with engine.begin() as conn: # Inicia uma transação automática + result = conn.execute(text(sql), params or {}) # Executa o SQL + + # Se for INSERT, retorna o último ID inserido + if sql.strip().upper().startswith("INSERT"): + last_id = conn.execute(text("SELECT LAST_INSERT_ID() AS id")).mappings().first() + return last_id + + # Se não for INSERT, retorna o primeiro registro + return result.mappings().first() + + except SQLAlchemyError as e: + print(f"[ERRO SQL]: {e}") # Imprime o erro para debug + raise + + def _execute( + self, + sql: str, + params: Optional[dict] = None, + fetch: Literal["all", "one", "result", "none"] = "result", + ): + """ + Método interno que executa o SQL no MySQL. + Suporta diferentes tipos de retorno: + - all -> todos os registros como lista de dicionários + - one -> primeiro registro como dicionário + - result -> ResultProxy bruto + - none -> não retorna nada + """ + engine = MySQL.get_engine() # Pega a engine do MySQL + + try: + with engine.connect() as conn: # Abre a conexão com o banco + result = conn.execute(text(sql), params or {}) # Executa o SQL com parâmetros + + # Commit explícito para operações DML + # Se não for um SELECT, faça o commit para persistir a alteração. + sql_upper = sql.strip().upper() + if sql_upper.startswith(("INSERT", "UPDATE", "DELETE")): + conn.commit() + + # Retorno baseado no tipo solicitado + if fetch == "all": + return result.mappings().all() # Todos os registros + elif fetch == "one": + return result.mappings().first() # Apenas o primeiro registro + elif fetch == "result": + return result # Retorno bruto do SQLAlchemy + elif fetch == "none": + return result.rowcount # Retorna o número de linhas afetadas + + except SQLAlchemyError as e: + print(f"[ERRO SQL]: {e}") # Log de erro para debug + raise diff --git a/actions/config/config.py b/actions/config/config.py new file mode 100644 index 0000000..177b551 --- /dev/null +++ b/actions/config/config.py @@ -0,0 +1,20 @@ +import json +from pathlib import Path +from types import SimpleNamespace + + +class Config: + + @staticmethod + def get(name: str): + # 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' / name + + # 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/actions/dynamic_import/dynamic_import.py b/actions/dynamic_import/dynamic_import.py new file mode 100644 index 0000000..0de32b5 --- /dev/null +++ b/actions/dynamic_import/dynamic_import.py @@ -0,0 +1,27 @@ +import importlib +from actions.config.config import Config + + +class DynamicImport: + + def __init__(self): + self.base = 'packages.v1' + + def set_package(self, name): + self.package = name + + def set_table(self, table): + self.table = table + + def service(self, name: str, class_name : str): + try: + # Define o nome do Módulo + module_file = f"{name}" + # Define o caminho do arquivo + path = f"{self.base}.{self.package}.services.{self.table}.{module_file}" + # Realiza a importação do arquivo + 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/actions/file/file.py b/actions/file/file.py new file mode 100644 index 0000000..0915c98 --- /dev/null +++ b/actions/file/file.py @@ -0,0 +1,32 @@ +import json +import os + + +class File: + + def create(self, data, caminho_arquivo='storage/temp.json'): + try: + # Garante que a pasta existe + os.makedirs(os.path.dirname(caminho_arquivo), exist_ok=True) + + # Lê dados existentes (ou cria nova lista) + if os.path.exists(caminho_arquivo): + with open(caminho_arquivo, 'r', encoding='utf-8') as arquivo: + try: + dados_existentes = json.load(arquivo) + if not isinstance(dados_existentes, list): + dados_existentes = [] + except json.JSONDecodeError: + dados_existentes = [] + else: + dados_existentes = [] + + # Adiciona novo dado + dados_existentes.append(data) + + # Salva novamente no arquivo com indentação + with open(caminho_arquivo, 'w', encoding='utf-8') as arquivo: + json.dump(dados_existentes, arquivo, indent=4, ensure_ascii=False) + + except Exception as e: + print(f"❌ Erro ao salvar o dado: {e}") \ No newline at end of file diff --git a/actions/jwt/create_token.py b/actions/jwt/create_token.py new file mode 100644 index 0000000..caca25d --- /dev/null +++ b/actions/jwt/create_token.py @@ -0,0 +1,36 @@ +from datetime import datetime, timedelta +from jose import jwt +from pytz import timezone + +from abstracts.action import BaseAction +from actions.config.config import Config + +class CreateToken(BaseAction): + def __init__(self): + # Busca as configurações da aplicação + self.config = Config.get('app.json') + + # Cria o timedelta com base na config + self.access_token_expire = timedelta( + minutes=self.config.jwt.expire.minute, + hours=self.config.jwt.expire.hours, + days=self.config.jwt.expire.days + ) + + def execute(self, tipo_token: str, data : str) -> str: + + sp = timezone('America/Sao_Paulo') + agora = datetime.now(tz=sp) + expira = agora + self.access_token_expire + + # Define os dados do token + payload = { + 'type' : tipo_token, + 'exp' : expira, + 'iat' : agora, + 'data' : str(data) + } + + # Retorna os dados codificados + return jwt.encode(payload, self.config.jwt.token, algorithm=self.config.jwt.algorithm) + \ No newline at end of file diff --git a/actions/jwt/get_current_user.py b/actions/jwt/get_current_user.py new file mode 100644 index 0000000..e70674d --- /dev/null +++ b/actions/jwt/get_current_user.py @@ -0,0 +1,24 @@ +from fastapi import Depends, HTTPException, status, Request +from fastapi.security import OAuth2PasswordBearer + +from actions.jwt.verify_token import VerifyToken # A classe que criamos anteriormente + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") # Apenas requerido pelo FastAPI + +def get_current_user(token: str = Depends(oauth2_scheme)): + + # Ação que válida o tokne + verify_token = VerifyToken() + # Obtem o resultado da validação + result = verify_token.execute(token) + + # Verifica se a resposta é diferente de inválida + if result['status'] != 'valid': + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=result.get('message', 'Token inválido ou expirado'), + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Retorna apenas os dados do token + return result['payload'] diff --git a/actions/jwt/verify_token.py b/actions/jwt/verify_token.py new file mode 100644 index 0000000..8c8d935 --- /dev/null +++ b/actions/jwt/verify_token.py @@ -0,0 +1,57 @@ +from datetime import datetime +from jose import jwt, JWTError, ExpiredSignatureError +from pytz import timezone + +from actions.config.config import Config + + +class VerifyToken: + def __init__(self): + # Carrega configurações + self.config = Config.get('app.json') + + def execute(self, token: str, expected_type: str = 'access-token') -> dict: + try: + # Decodifica o token + payload = jwt.decode( + token, + self.config.jwt.token, + algorithms=[self.config.jwt.algorithm] + ) + + # Valida expiração + exp_timestamp = payload.get("exp") + if exp_timestamp is None: + raise ValueError("O token não possui data de expiração.") + + # Verifica o tipo de token + token_type = payload.get("type") + if token_type != expected_type: + raise ValueError("Tipo de token inválido.") + + # Verificação opcional: validar campo "data" + if "data" not in payload: + raise ValueError("Token malformado: campo 'data' ausente.") + + return { + "status": "valid", + "payload": payload + } + + except ExpiredSignatureError: + return { + "status": "expired", + "message": "O token expirou." + } + + except JWTError as e: + return { + "status": "invalid", + "message": f"Token inválido: {str(e)}" + } + + except Exception as e: + return { + "status": "error", + "message": f"Erro na validação do token: {str(e)}" + } \ No newline at end of file diff --git a/actions/log/log.py b/actions/log/log.py new file mode 100644 index 0000000..3fe4298 --- /dev/null +++ b/actions/log/log.py @@ -0,0 +1,32 @@ +import json +import os + + +class Log: + + def register(self, data, caminho_arquivo='storage/temp.json'): + try: + # Garante que a pasta existe + os.makedirs(os.path.dirname(caminho_arquivo), exist_ok=True) + + # Lê dados existentes (ou cria nova lista) + if os.path.exists(caminho_arquivo): + with open(caminho_arquivo, 'r', encoding='utf-8') as arquivo: + try: + dados_existentes = json.load(arquivo) + if not isinstance(dados_existentes, list): + dados_existentes = [] + except json.JSONDecodeError: + dados_existentes = [] + else: + dados_existentes = [] + + # Adiciona novo dado + dados_existentes.append(data) + + # Salva novamente no arquivo com indentação + with open(caminho_arquivo, 'w', encoding='utf-8') as arquivo: + json.dump(dados_existentes, arquivo, indent=4, ensure_ascii=False) + + except Exception as e: + print(f"❌ Erro ao salvar o dado: {e}") \ No newline at end of file diff --git a/actions/security/security.py b/actions/security/security.py new file mode 100644 index 0000000..9358046 --- /dev/null +++ b/actions/security/security.py @@ -0,0 +1,43 @@ +# core/security.py + +# Importa CryptContext da biblioteca passlib para operações de hash de senha +from passlib.context import CryptContext + +# Cria uma instância do contexto de criptografia +# O esquema usado é 'bcrypt', que é seguro e amplamente aceito +# O parâmetro 'deprecated="auto"' marca versões antigas como inseguras, se aplicável +CRYPTO = CryptContext(schemes=['bcrypt'], deprecated='auto') + + +class Security: + + # Verifica se a senha tem um hash válido + @staticmethod + def is_hash(senha: str) -> bool: + """ + Verifica se a string fornecida é um hash reconhecido pelo CryptContext. + """ + return CRYPTO.identify(senha) + + + # Verifica se uma senha fornecida corresponde ao hash armazenado + def verify_password(plain_password: str, hashed_password: str) -> bool: + """ + Compara a senha fornecida em texto puro com o hash armazenado. + + :param plain_password: Senha digitada pelo usuário + :param hashed_password: Hash da senha armazenado no banco de dados + :return: True se corresponder, False se não + """ + return CRYPTO.verify(plain_password, hashed_password) + + + # Gera o hash de uma senha fornecida + def hash_password(plain_password: str) -> str: + """ + Gera e retorna o hash da senha fornecida. + + :param plain_password: Senha em texto puro fornecida pelo usuário + :return: Hash da senha + """ + return CRYPTO.hash(plain_password) diff --git a/actions/system/exceptions.py b/actions/system/exceptions.py new file mode 100644 index 0000000..ec151c9 --- /dev/null +++ b/actions/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/actions/system/handlers.py b/actions/system/handlers.py new file mode 100644 index 0000000..3e2cfe4 --- /dev/null +++ b/actions/system/handlers.py @@ -0,0 +1,86 @@ +# handlers.py +import json +import traceback + +from fastapi import Request +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse +from starlette.exceptions import HTTPException as StarletteHTTPException + +from actions.system.exceptions import BusinessRuleException +from actions.log.log import Log + + +def register_exception_handlers(app): + + def __init__ (self): + log = Log() + + @app.exception_handler(BusinessRuleException) + async def business_rule_exception_handler(request: Request, exc: BusinessRuleException): + + response = { + "status": "422", + "error": "Regra de negócio", + "detail": exc.message + } + + # Salva o log em disco + Log.register(response, 'storage/temp/business_rule_exception_handler.json') + + return JSONResponse( + status_code=422, + content=response + ) + + @app.exception_handler(StarletteHTTPException) + async def http_exception_handler(request: Request, exc: StarletteHTTPException): + response = { + "status": exc.status_code, + "error": "HTTP Error", + "detail": exc.detail + } + + # Salva o log em disco + Log.register(response, 'storage/temp/http_exception_handler.json') + + return JSONResponse( + status_code=exc.status_code, + content=response + ) + + @app.exception_handler(RequestValidationError) + async def validation_exception_handler(request: Request, exc: RequestValidationError): + + response = { + "status": 400, + "error": "Erro de validação", + "detail": exc.errors() + } + + # Salva o log em disco + Log.register(response, 'storage/temp/validation_exception_handler.json') + + return JSONResponse( + status_code=400, + content=response + ) + + @app.exception_handler(Exception) + async def global_exception_handler(request: Request, exc: Exception): + + response = { + "status": 500, + "error": "Erro Interno do Servidor", + "type": type(exc).__name__, + "message": str(exc), + "trace": traceback.format_exc() + } + + # Salva o log em disco + Log.register(response, 'storage/temp/validation_exception_handler.json') + + return JSONResponse( + status_code=500, + content=response + ) diff --git a/actions/validations/cep.py b/actions/validations/cep.py new file mode 100644 index 0000000..025eefc --- /dev/null +++ b/actions/validations/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/actions/validations/cnpj.py b/actions/validations/cnpj.py new file mode 100644 index 0000000..2d5b190 --- /dev/null +++ b/actions/validations/cnpj.py @@ -0,0 +1,35 @@ +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/actions/validations/cpf.py b/actions/validations/cpf.py new file mode 100644 index 0000000..2083800 --- /dev/null +++ b/actions/validations/cpf.py @@ -0,0 +1,34 @@ +import re + + +class CPF: + + @staticmethod + def is_valid_cpf(data: str) -> bool: + # Remove caracteres não numéricos + data = re.sub(r'\D', '', data) + + # Verifica se tem 11 dígitos + if len(data) != 11: + return False + + # CPFs com todos os dígitos iguais são inválidos + if data == data[0] * 11: + return False + + # Calcula o primeiro e segundo dígitos verificadores + def calcular_digito(digitos, peso): + soma = sum(int(a) * b for a, b in zip(digitos, peso)) + resto = soma % 11 + return '0' if resto < 2 else str(11 - resto) + + # Primeiro dígito verificador + peso1 = range(10, 1, -1) + digito1 = calcular_digito(data[:9], peso1) + + # Segundo dígito verificador + peso2 = range(11, 1, -1) + digito2 = calcular_digito(data[:10], peso2) + + # Verifica se os dígitos batem + return data[-2:] == digito1 + digito2 \ No newline at end of file diff --git a/actions/validations/email.py b/actions/validations/email.py new file mode 100644 index 0000000..4839b95 --- /dev/null +++ b/actions/validations/email.py @@ -0,0 +1,9 @@ +import re + + +class Email: + + @staticmethod + def is_valid_email(email: str) -> bool: + """Check if email has a valid structure""" + return bool(re.match(r"^[\w\.-]+@[\w\.-]+\.\w+$", email)) \ No newline at end of file diff --git a/actions/validations/phone.py b/actions/validations/phone.py new file mode 100644 index 0000000..b509c7f --- /dev/null +++ b/actions/validations/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/actions/validations/text.py b/actions/validations/text.py new file mode 100644 index 0000000..51438bc --- /dev/null +++ b/actions/validations/text.py @@ -0,0 +1,63 @@ +import html +import re + + +class Text: + + # Remove as mascaras de números + @staticmethod + def just_numbers(data: str) -> str: + """ Mantêm apenas os numeros """ + data = re.sub(r"[^\d]", "", data) + return data + + # Verifica se um e-mail é válido + @staticmethod + def is_valid_email(email: str) -> bool: + """Check if email has a valid structure""" + return bool(re.match(r"^[\w\.-]+@[\w\.-]+\.\w+$", email)) + + + """ + Sanitiza entradas de texto contra XSS e SQL Injection básicos. + - Remove espaços extras + - Escapa entidades HTML + - Remove padrões suspeitos de XSS e SQL Injection + - Normaliza múltiplos espaços em um só + """ + @staticmethod + def sanitize_input(data: str) -> str: + + if not data: + return data + + # 1) Remove espaços no início e no fim + data = data.strip() + + # 2) Escapa entidades HTML (< > & ") + data = html.escape(data) + + # 3) Remove múltiplos espaços seguidos + data = re.sub(r"\s+", " ", data) + + # 4) Remove tags