diff --git a/abstracts/action.py b/abstracts/action.py index da31d8a..2642a82 100644 --- a/abstracts/action.py +++ b/abstracts/action.py @@ -2,7 +2,6 @@ from abc import ABC, abstractmethod class BaseAction: - """ Classe abstrata base para todos as actions do sistema. Obriga implementação de um método execute(). @@ -14,4 +13,4 @@ class BaseAction: 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 + pass diff --git a/abstracts/repository.py b/abstracts/repository.py index 59d6507..babe4c0 100644 --- a/abstracts/repository.py +++ b/abstracts/repository.py @@ -22,7 +22,7 @@ class BaseRepository: def run(self, sql: str, params: Optional[dict] = None): """Executa um SQL sem retorno de dados (ex: INSERT, UPDATE, DELETE)""" return self._execute(sql, params, fetch="none") - + def run_and_return(self, sql: str, params: Optional[dict] = None): """Executa SQL com RETURNING e retorna o primeiro registro como dict""" return self._execute(sql, params, fetch="one") @@ -47,4 +47,3 @@ class BaseRepository: except SQLAlchemyError as e: print(f"[ERRO SQL]: {e}") raise - \ No newline at end of file diff --git a/actions/config/config.py b/actions/config/config.py index 177b551..dbaba2e 100644 --- a/actions/config/config.py +++ b/actions/config/config.py @@ -11,10 +11,10 @@ class Config: 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 + config_path = base_dir.parent.parent / "config" / name # Carrega o JSON como objeto acessível por ponto - with open(config_path, 'r') as f: + 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 + return config diff --git a/actions/dynamic_import/dynamic_import.py b/actions/dynamic_import/dynamic_import.py index fddc643..9df5ec0 100644 --- a/actions/dynamic_import/dynamic_import.py +++ b/actions/dynamic_import/dynamic_import.py @@ -1,7 +1,7 @@ import importlib -from actions.config.config import Config -from typing import Optional, Any, Type +from typing import Any, Optional, Type +from actions.config.config import Config class DynamicImport: @@ -18,7 +18,7 @@ class DynamicImport: def set_table(self, table: str): self.table = table - def service(self, name: str, class_name : str) -> Type[Any]: + def service(self, name: str, class_name: str) -> Type[Any]: try: # Define o nome do Módulo module_file = f"{name}" diff --git a/actions/file/file.py b/actions/file/file.py index 0915c98..019ba21 100644 --- a/actions/file/file.py +++ b/actions/file/file.py @@ -4,14 +4,14 @@ import os class File: - def create(self, data, caminho_arquivo='storage/temp.json'): + 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: + with open(caminho_arquivo, "r", encoding="utf-8") as arquivo: try: dados_existentes = json.load(arquivo) if not isinstance(dados_existentes, list): @@ -25,8 +25,8 @@ class File: dados_existentes.append(data) # Salva novamente no arquivo com indentação - with open(caminho_arquivo, 'w', encoding='utf-8') as arquivo: + 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 + print(f"❌ Erro ao salvar o dado: {e}") diff --git a/actions/jwt/create_token.py b/actions/jwt/create_token.py index caca25d..c0deae1 100644 --- a/actions/jwt/create_token.py +++ b/actions/jwt/create_token.py @@ -1,36 +1,34 @@ 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') + 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 + days=self.config.jwt.expire.days, ) - def execute(self, tipo_token: str, data : str) -> str: - - sp = timezone('America/Sao_Paulo') + 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) - } + 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 + return jwt.encode( + payload, self.config.jwt.token, algorithm=self.config.jwt.algorithm + ) diff --git a/actions/jwt/get_current_user.py b/actions/jwt/get_current_user.py index e70674d..79e3f62 100644 --- a/actions/jwt/get_current_user.py +++ b/actions/jwt/get_current_user.py @@ -1,10 +1,12 @@ -from fastapi import Depends, HTTPException, status, Request +from fastapi import Depends, HTTPException, Request, status from fastapi.security import OAuth2PasswordBearer -from actions.jwt.verify_token import VerifyToken # A classe que criamos anteriormente +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 @@ -13,12 +15,12 @@ def get_current_user(token: str = Depends(oauth2_scheme)): result = verify_token.execute(token) # Verifica se a resposta é diferente de inválida - if result['status'] != 'valid': + if result["status"] != "valid": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail=result.get('message', 'Token inválido ou expirado'), + detail=result.get("message", "Token inválido ou expirado"), headers={"WWW-Authenticate": "Bearer"}, ) # Retorna apenas os dados do token - return result['payload'] + return result["payload"] diff --git a/actions/jwt/verify_token.py b/actions/jwt/verify_token.py index 8c8d935..b20f5b1 100644 --- a/actions/jwt/verify_token.py +++ b/actions/jwt/verify_token.py @@ -1,5 +1,6 @@ from datetime import datetime -from jose import jwt, JWTError, ExpiredSignatureError + +from jose import ExpiredSignatureError, JWTError, jwt from pytz import timezone from actions.config.config import Config @@ -8,15 +9,13 @@ from actions.config.config import Config class VerifyToken: def __init__(self): # Carrega configurações - self.config = Config.get('app.json') + self.config = Config.get("app.json") - def execute(self, token: str, expected_type: str = 'access-token') -> dict: + 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] + token, self.config.jwt.token, algorithms=[self.config.jwt.algorithm] ) # Valida expiração @@ -33,25 +32,16 @@ class VerifyToken: if "data" not in payload: raise ValueError("Token malformado: campo 'data' ausente.") - return { - "status": "valid", - "payload": payload - } + return {"status": "valid", "payload": payload} except ExpiredSignatureError: - return { - "status": "expired", - "message": "O token expirou." - } + return {"status": "expired", "message": "O token expirou."} except JWTError as e: - return { - "status": "invalid", - "message": f"Token inválido: {str(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 + "message": f"Erro na validação do token: {str(e)}", + } diff --git a/actions/log/log.py b/actions/log/log.py index 3fe4298..e15950e 100644 --- a/actions/log/log.py +++ b/actions/log/log.py @@ -4,14 +4,14 @@ import os class Log: - def register(self, data, caminho_arquivo='storage/temp.json'): + 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: + with open(caminho_arquivo, "r", encoding="utf-8") as arquivo: try: dados_existentes = json.load(arquivo) if not isinstance(dados_existentes, list): @@ -25,8 +25,8 @@ class Log: dados_existentes.append(data) # Salva novamente no arquivo com indentação - with open(caminho_arquivo, 'w', encoding='utf-8') as arquivo: + 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 + print(f"❌ Erro ao salvar o dado: {e}") diff --git a/actions/security/security.py b/actions/security/security.py index e23c3a6..8e46663 100644 --- a/actions/security/security.py +++ b/actions/security/security.py @@ -6,7 +6,7 @@ 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') +CRYPTO = CryptContext(schemes=["bcrypt"], deprecated="auto") class Security: @@ -19,19 +19,17 @@ class Security: """ return CRYPTO.identify(senha) - # Verifica se uma senha fornecida corresponde ao hash armazenado def verify_senha_api(plain_senha_api: str, hashed_senha_api: str) -> bool: """ Compara a senha fornecida em texto puro com o hash armazenado. - + :param plain_senha_api: Senha digitada pelo usuário :param hashed_senha_api: Hash da senha armazenado no banco de dados :return: True se corresponder, False se não """ return CRYPTO.verify(plain_senha_api, hashed_senha_api) - # Gera o hash de uma senha fornecida def hash_senha_api(plain_senha_api: str) -> str: """ diff --git a/actions/system/exceptions.py b/actions/system/exceptions.py index ec151c9..741dbb5 100644 --- a/actions/system/exceptions.py +++ b/actions/system/exceptions.py @@ -1,4 +1,4 @@ # exceptions.py class BusinessRuleException(Exception): def __init__(self, message: str): - self.message = message \ No newline at end of file + self.message = message diff --git a/actions/system/handlers.py b/actions/system/handlers.py index 3e2cfe4..bbd8834 100644 --- a/actions/system/handlers.py +++ b/actions/system/handlers.py @@ -7,80 +7,64 @@ 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 +from actions.system.exceptions import BusinessRuleException def register_exception_handlers(app): - def __init__ (self): + def __init__(self): log = Log() @app.exception_handler(BusinessRuleException) - async def business_rule_exception_handler(request: Request, exc: BusinessRuleException): + async def business_rule_exception_handler( + request: Request, exc: BusinessRuleException + ): + + response = {"status": "422", "error": "Regra de negócio", "detail": exc.message} - 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') + Log.register(response, "storage/temp/business_rule_exception_handler.json") - return JSONResponse( - status_code=422, - content=response - ) + 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 - } + "status": exc.status_code, + "error": "HTTP Error", + "detail": exc.detail, + } # Salva o log em disco - Log.register(response, 'storage/temp/http_exception_handler.json') + Log.register(response, "storage/temp/http_exception_handler.json") - return JSONResponse( - status_code=exc.status_code, - content=response - ) + return JSONResponse(status_code=exc.status_code, content=response) @app.exception_handler(RequestValidationError) - async def validation_exception_handler(request: Request, exc: RequestValidationError): + async def validation_exception_handler( + request: Request, exc: RequestValidationError + ): - response = { - "status": 400, - "error": "Erro de validação", - "detail": exc.errors() - } + 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') + Log.register(response, "storage/temp/validation_exception_handler.json") - return JSONResponse( - status_code=400, - content=response - ) + 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() - } + "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') + Log.register(response, "storage/temp/validation_exception_handler.json") - return JSONResponse( - status_code=500, - content=response - ) + return JSONResponse(status_code=500, content=response) diff --git a/actions/validations/cep.py b/actions/validations/cep.py index 025eefc..21bf1c4 100644 --- a/actions/validations/cep.py +++ b/actions/validations/cep.py @@ -1,8 +1,7 @@ - class CEP: @staticmethod def validate(data: str) -> bool: # Valida e retorna a informação - return len(data) == 8 \ No newline at end of file + return len(data) == 8 diff --git a/actions/validations/cnpj.py b/actions/validations/cnpj.py index 2d5b190..8f48707 100644 --- a/actions/validations/cnpj.py +++ b/actions/validations/cnpj.py @@ -7,7 +7,7 @@ class CNPJ: def validate(data: str) -> bool: # Remove caracteres não numéricos - data = re.sub(r'\D', '', data) + data = re.sub(r"\D", "", data) # Verifica se tem 14 dígitos if len(data) != 14: @@ -21,7 +21,7 @@ class CNPJ: 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) + 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] diff --git a/actions/validations/cpf.py b/actions/validations/cpf.py index 2083800..6b06390 100644 --- a/actions/validations/cpf.py +++ b/actions/validations/cpf.py @@ -6,7 +6,7 @@ class CPF: @staticmethod def is_valid_cpf(data: str) -> bool: # Remove caracteres não numéricos - data = re.sub(r'\D', '', data) + data = re.sub(r"\D", "", data) # Verifica se tem 11 dígitos if len(data) != 11: @@ -20,7 +20,7 @@ class CPF: 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) + return "0" if resto < 2 else str(11 - resto) # Primeiro dígito verificador peso1 = range(10, 1, -1) @@ -31,4 +31,4 @@ class CPF: digito2 = calcular_digito(data[:10], peso2) # Verifica se os dígitos batem - return data[-2:] == digito1 + digito2 \ No newline at end of file + return data[-2:] == digito1 + digito2 diff --git a/actions/validations/email.py b/actions/validations/email.py index 4839b95..9786935 100644 --- a/actions/validations/email.py +++ b/actions/validations/email.py @@ -6,4 +6,4 @@ 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 + return bool(re.match(r"^[\w\.-]+@[\w\.-]+\.\w+$", email)) diff --git a/actions/validations/phone.py b/actions/validations/phone.py index b509c7f..07b9885 100644 --- a/actions/validations/phone.py +++ b/actions/validations/phone.py @@ -1,4 +1,3 @@ - class Phone: @staticmethod @@ -9,4 +8,4 @@ class Phone: @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 + return len(data) == 10 diff --git a/actions/validations/text.py b/actions/validations/text.py index 51438bc..20fd338 100644 --- a/actions/validations/text.py +++ b/actions/validations/text.py @@ -7,16 +7,15 @@ class Text: # Remove as mascaras de números @staticmethod def just_numbers(data: str) -> str: - """ Mantêm apenas os numeros """ + """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. @@ -25,6 +24,7 @@ class Text: - Remove padrões suspeitos de XSS e SQL Injection - Normaliza múltiplos espaços em um só """ + @staticmethod def sanitize_input(data: str) -> str: @@ -41,23 +41,36 @@ class Text: data = re.sub(r"\s+", " ", data) # 4) Remove tags