Primeiro commit
This commit is contained in:
commit
28c7462d2f
25 changed files with 1605 additions and 0 deletions
9
.gitattributes
vendored
Normal file
9
.gitattributes
vendored
Normal file
|
|
@ -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
|
||||
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
|
|
@ -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/
|
||||
26
Dockerfile
Normal file
26
Dockerfile
Normal file
|
|
@ -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"]
|
||||
1
README.md
Normal file
1
README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# apiorius
|
||||
18
api/v1/api.py
Normal file
18
api/v1/api.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from fastapi import APIRouter # Importa o gerenciador de rotas do FastAPI
|
||||
|
||||
# Importa os módulos de rotas específicos
|
||||
from api.v1.endpoints import g_usuario_endpoint
|
||||
from api.v1.endpoints import c_caixa_item_endpoint
|
||||
|
||||
# Cria uma instância do APIRouter que vai agregar todas as rotas da API
|
||||
api_router = APIRouter()
|
||||
|
||||
# Inclui as rotas de "g_usuario" no roteador principal, com prefixo /usuarios e tag 'Usuarios'
|
||||
api_router.include_router(
|
||||
g_usuario_endpoint.router, prefix='/usuarios', tags=['Usuários']
|
||||
)
|
||||
|
||||
# Inclui as rotas de "c_caixa_item no roteador principal, com prefixo /c_caixa_items e tag 'Caixa Itens'
|
||||
api_router.include_router(
|
||||
c_caixa_item_endpoint.router, prefix='/caixa_itens', tags=['Caixa Itens']
|
||||
)
|
||||
0
api/v1/controllers/caixa/atos_praticados.py
Normal file
0
api/v1/controllers/caixa/atos_praticados.py
Normal file
69
api/v1/controllers/caixa/c_caixa_item_controller.py
Normal file
69
api/v1/controllers/caixa/c_caixa_item_controller.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
from typing import Optional, List
|
||||
from fastapi import HTTPException, status # Importe HTTPException e status
|
||||
|
||||
# Schemas usados para entrada e saída de dados dos items
|
||||
from api.v1.schemas.caixa.c_caixa_item_schema import (
|
||||
CCaixaItemSchemaBase,
|
||||
CCaixaItemSchemaList,
|
||||
CCaixaItemPaginationSchema
|
||||
)
|
||||
|
||||
# Model responsável pelo acesso ao banco de dados Firebird
|
||||
from api.v1.models.caixa.c_caixa_item_model import CCaixaItemModel
|
||||
|
||||
# Funções para sanitização de entradas (evitar XSS, SQLi etc.)
|
||||
from core.validation import InputSanitizer
|
||||
|
||||
# Retorna a lista de todos os itens cadastrados
|
||||
def get_all_caixa_itens(skip: int = 0, limit: int = 10) -> CCaixaItemPaginationSchema:
|
||||
try:
|
||||
itens = CCaixaItemModel.get_all_caixa_itens(skip=skip, limit=limit)
|
||||
total = CCaixaItemModel.count_items()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"skip": skip,
|
||||
"limit": limit,
|
||||
"data": [CCaixaItemSchemaList(**u) for u in itens]
|
||||
}
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erro interno do servidor ao listar os itens: {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Ocorreu um erro inesperado ao listar os itens: {e}"
|
||||
)
|
||||
|
||||
# Retorna a quantidade de registros no banco de dados
|
||||
def count_items() -> int:
|
||||
try:
|
||||
return CCaixaItemModel.count_items()
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erro ao contar items: {e}"
|
||||
)
|
||||
|
||||
# Retorna um item específico pelo ID
|
||||
def get_item_by_id(caixa_item_id: int) -> CCaixaItemSchemaBase: # Retorno alterado para UserSchemaBase
|
||||
try:
|
||||
item = CCaixaItemModel.get_by_id(caixa_item_id)
|
||||
if not item:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Item com ID {caixa_item_id} não encontrado."
|
||||
)
|
||||
return CCaixaItemSchemaBase(**item)
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erro interno do servidor ao buscar item por ID: {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Ocorreu um erro inesperado ao buscar item por ID: {e}"
|
||||
)
|
||||
239
api/v1/controllers/g_usuario_controller.py
Normal file
239
api/v1/controllers/g_usuario_controller.py
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
# controllers/user_controller.py
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import HTTPException, status # Importe HTTPException e status
|
||||
|
||||
# Schemas usados para entrada e saída de dados dos usuários
|
||||
from api.v1.schemas.g_usuario_schema import (
|
||||
UserSchemaBase,
|
||||
UserSchemaCreate,
|
||||
UserSchemaUpdate,
|
||||
UserSchemaList,
|
||||
UserPaginationSchema
|
||||
)
|
||||
|
||||
# Model responsável pelo acesso ao banco de dados Firebird
|
||||
from api.v1.models.g_usuario_model import UserModel
|
||||
|
||||
# Funções utilitárias para segurança (hash e verificação de senha)
|
||||
from core.security import verify_senha_api, hash_senha_api
|
||||
|
||||
# Funções para sanitização de entradas (evitar XSS, SQLi etc.)
|
||||
from core.validation import InputSanitizer
|
||||
|
||||
|
||||
# Autentica um usuário com base no e-mail e senha fornecidos
|
||||
def authenticate_user(email: str, senha_api: str) -> Optional[dict]:
|
||||
# Nenhuma mudança significativa aqui, pois o retorno já é None ou dict
|
||||
email = InputSanitizer.clean_text(email)
|
||||
try:
|
||||
user = UserModel.get_by_email(email)
|
||||
if not user:
|
||||
return None # Não lança exceção para 'usuário não encontrado' em autenticação
|
||||
|
||||
if not verify_senha_api(senha_api, user["senha_api"]):
|
||||
return None
|
||||
|
||||
return user
|
||||
except RuntimeError as e:
|
||||
# Erros no banco de dados durante a busca, que podem ser tratados como 500
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erro interno do servidor ao autenticar: {e}"
|
||||
)
|
||||
|
||||
|
||||
# Cria um novo usuário após validação e sanitização dos campos
|
||||
def create_user(user_data: UserSchemaCreate) -> UserSchemaBase: # Retorno alterado para UserSchemaBase, não Optional
|
||||
try:
|
||||
# Sanitiza os campos recebidos
|
||||
nome_completo = InputSanitizer.clean_text(user_data.nome_completo)
|
||||
email = InputSanitizer.clean_text(user_data.email)
|
||||
senha_api = InputSanitizer.clean_text(user_data.senha_api)
|
||||
|
||||
# Validações iniciais (antes de ir para o banco)
|
||||
if not InputSanitizer.is_valid_email(email):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Formato de e-mail inválido."
|
||||
)
|
||||
|
||||
if not InputSanitizer.is_safe(nome_completo + email + senha_api):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Conteúdo malicioso detectado nos dados do usuário."
|
||||
)
|
||||
|
||||
hashed_senha_api = hash_senha_api(senha_api)
|
||||
|
||||
# Chama o método do modelo, que agora pode levantar exceções
|
||||
result = UserModel.create(nome_completo, email, hashed_senha_api)
|
||||
|
||||
# Se o modelo retornar um dicionário, converte para UserSchemaBase
|
||||
if result:
|
||||
return UserSchemaBase(**result)
|
||||
# Se o modelo retornasse None (o que não deve mais acontecer com as exceções),
|
||||
# poderia ser um erro interno ou algo que não previu.
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Não foi possível criar o usuário por uma razão desconhecida."
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
# Captura erros de validação de dados do modelo (ex: e-mail duplicado, ou ID problemático)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, # 409 Conflict para dados duplicados
|
||||
detail=str(e)
|
||||
)
|
||||
except RuntimeError as e:
|
||||
# Captura erros gerais de banco de dados ou problemas inesperados do modelo
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erro interno do servidor ao criar usuário: {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
# Captura qualquer outra exceção não tratada especificamente
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Ocorreu um erro inesperado: {e}"
|
||||
)
|
||||
|
||||
|
||||
# Retorna a lista de todos os usuários cadastrados
|
||||
def get_all(skip: int = 0, limit: int = 10) -> UserPaginationSchema:
|
||||
try:
|
||||
users = UserModel.get_all(skip=skip, limit=limit)
|
||||
total = UserModel.count_users()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"skip": skip,
|
||||
"limit": limit,
|
||||
"data": [UserSchemaList(**u) for u in users]
|
||||
}
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erro interno do servidor ao listar usuários: {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Ocorreu um erro inesperado ao listar usuários: {e}"
|
||||
)
|
||||
|
||||
# Retorna a quantidade de registros no banco de dados
|
||||
def count_users() -> int:
|
||||
try:
|
||||
return UserModel.count_users()
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erro ao contar usuários: {e}"
|
||||
)
|
||||
|
||||
# Retorna um usuário específico pelo ID
|
||||
def get_user_by_id(user_id: int) -> UserSchemaBase: # Retorno alterado para UserSchemaBase
|
||||
try:
|
||||
user = UserModel.get_by_id(user_id)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Usuário com ID {user_id} não encontrado."
|
||||
)
|
||||
return UserSchemaBase(**user)
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erro interno do servidor ao buscar usuário por ID: {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Ocorreu um erro inesperado ao buscar usuário por ID: {e}"
|
||||
)
|
||||
|
||||
|
||||
# Atualiza os dados de um usuário existente
|
||||
def update_user(user_id: int, user_data: UserSchemaUpdate) -> UserSchemaBase: # Retorno alterado
|
||||
try:
|
||||
nome_completo = InputSanitizer.clean_text(user_data.nome_completo) if user_data.nome_completo else None
|
||||
email = InputSanitizer.clean_text(user_data.email) if user_data.email else None
|
||||
senha_api = InputSanitizer.clean_text(user_data.senha_api) if user_data.senha_api else None
|
||||
telefone = InputSanitizer.clean_text(user_data.telefone) if user_data.telefone else None
|
||||
|
||||
if email and not InputSanitizer.is_valid_email(email):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Formato de e-mail inválido para atualização."
|
||||
)
|
||||
|
||||
if any([nome_completo, email, senha_api]) and not InputSanitizer.is_safe(
|
||||
(nome_completo or '') + (email or '') + (senha_api or '')
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Conteúdo malicioso detectado nos dados de atualização."
|
||||
)
|
||||
|
||||
hashed_senha_api = hash_senha_api(senha_api) if senha_api else None
|
||||
|
||||
success = UserModel.update(user_id, nome_completo, email, hashed_senha_api, telefone)
|
||||
if not success:
|
||||
# UserModel.update agora lança KeyError/ValueError, então este 'if not success'
|
||||
# só seria acionado se houvesse um caso em que nada foi atualizado e não houve erro.
|
||||
# No entanto, com a lógica atual de raise, ele não deve ser alcançado.
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, # Ou 422 Unprocessable Entity
|
||||
detail="Nenhum dado válido fornecido para atualização."
|
||||
)
|
||||
|
||||
return get_user_by_id(user_id) # Se atualizou com sucesso, retorna o usuário atualizado
|
||||
|
||||
except KeyError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e)
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, # Conflito de dados (ex: email duplicado)
|
||||
detail=str(e)
|
||||
)
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erro interno do servidor ao atualizar usuário: {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Ocorreu um erro inesperado ao atualizar usuário: {e}"
|
||||
)
|
||||
|
||||
|
||||
# Deleta um usuário do banco de dados pelo ID
|
||||
def delete_user(user_id: int) -> bool:
|
||||
try:
|
||||
success = UserModel.delete(user_id)
|
||||
if not success: # Embora com KeyError do modelo, esta linha talvez não seja mais alcançada
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Usuário com ID {user_id} não encontrado para exclusão."
|
||||
)
|
||||
return success
|
||||
except KeyError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e)
|
||||
)
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erro interno do servidor ao excluir usuário: {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Ocorreu um erro inesperado ao excluir usuário: {e}"
|
||||
)
|
||||
55
api/v1/endpoints/c_caixa_item_endpoint.py
Normal file
55
api/v1/endpoints/c_caixa_item_endpoint.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# endpoints/c_caixa_item_endpoint.py
|
||||
|
||||
from typing import List
|
||||
from fastapi import APIRouter, status, Depends, HTTPException, Response, Query
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
# Schemas para entrada e saída de dados (nomes padronizados em inglês)
|
||||
from api.v1.schemas.caixa.c_caixa_item_schema import (
|
||||
CCaixaItemSchemaBase,
|
||||
CCaixaItemSchemaList,
|
||||
CCaixaItemPaginationSchema
|
||||
)
|
||||
|
||||
# Controller responsável pelas regras de negócio e sanitização
|
||||
from api.v1.controllers.caixa.c_caixa_item_controller import (
|
||||
get_all_caixa_itens,
|
||||
get_item_by_id,
|
||||
count_items
|
||||
)
|
||||
|
||||
# Dependência para obter o usuário autenticado a partir do token JWT
|
||||
from core.deps import get_current_user
|
||||
|
||||
# Inicializa o roteador responsável pelas rotas de usuários
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
|
||||
# ---------------------- ROTAS DINÂMICAS ----------------------
|
||||
|
||||
@router.get('/', response_model=CCaixaItemPaginationSchema)
|
||||
def get_items(skip: int = Query(0, ge=0), limit: int = Query(10, ge=1), current_user: dict = Depends(get_current_user)):
|
||||
"""
|
||||
Retorna todos os usuários cadastrados no sistema.
|
||||
"""
|
||||
items = get_all_caixa_itens(skip=skip, limit=limit)
|
||||
total = count_items()
|
||||
|
||||
return get_all_caixa_itens(skip=skip, limit=limit)
|
||||
|
||||
|
||||
@router.get('/{caixa_item_id}', response_model=CCaixaItemSchemaBase, status_code=status.HTTP_200_OK)
|
||||
def get_user(caixa_item_id: int, current_user: dict = Depends(get_current_user)):
|
||||
"""
|
||||
Retorna os dados de um caixa item específico pelo ID.
|
||||
"""
|
||||
item = get_item_by_id(caixa_item_id)
|
||||
if item:
|
||||
return item
|
||||
|
||||
raise HTTPException(
|
||||
detail='User not found.',
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
138
api/v1/endpoints/g_usuario_endpoint.py
Normal file
138
api/v1/endpoints/g_usuario_endpoint.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# endpoints/g_usuario_endpoint.py
|
||||
|
||||
from typing import List
|
||||
from fastapi import APIRouter, status, Depends, HTTPException, Response, Query
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
# Schemas para entrada e saída de dados (nomes padronizados em inglês)
|
||||
from api.v1.schemas.g_usuario_schema import (
|
||||
UserSchemaBase,
|
||||
UserSchemaCreate,
|
||||
UserSchemaUpdate,
|
||||
UserPaginationSchema
|
||||
)
|
||||
|
||||
# Controller responsável pelas regras de negócio e sanitização
|
||||
from api.v1.controllers.g_usuario_controller import (
|
||||
authenticate_user,
|
||||
create_user,
|
||||
get_all,
|
||||
get_user_by_id,
|
||||
update_user,
|
||||
delete_user,
|
||||
count_users
|
||||
)
|
||||
|
||||
# Dependência para obter o usuário autenticado a partir do token JWT
|
||||
from core.deps import get_current_user
|
||||
|
||||
# Função para gerar JWT
|
||||
from core.auth import create_access_token
|
||||
|
||||
# Inicializa o roteador responsável pelas rotas de usuários
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ---------------------- ROTAS FIXAS ----------------------
|
||||
|
||||
@router.get('/logado', response_model=UserSchemaBase)
|
||||
def get_logged_user(current_user: dict = Depends(get_current_user)):
|
||||
"""
|
||||
Retorna os dados do usuário autenticado com o token atual.
|
||||
"""
|
||||
return current_user
|
||||
|
||||
|
||||
@router.post('/signup', status_code=status.HTTP_201_CREATED, response_model=UserSchemaBase)
|
||||
def post_user(user: UserSchemaCreate):
|
||||
"""
|
||||
Cria um novo usuário após validações e sanitizações.
|
||||
"""
|
||||
new_user = create_user(user)
|
||||
if not new_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_406_NOT_ACCEPTABLE,
|
||||
detail='E-mail is already registered.'
|
||||
)
|
||||
return new_user
|
||||
|
||||
|
||||
@router.post('/login')
|
||||
def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
"""
|
||||
Realiza login com e-mail e senha, retornando um token JWT válido.
|
||||
"""
|
||||
user = authenticate_user(
|
||||
email=form_data.username,
|
||||
senha_api=form_data.password
|
||||
)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Invalid login credentials.'
|
||||
)
|
||||
|
||||
return JSONResponse(content={
|
||||
"access_token": create_access_token(sub=user["user_id"]),
|
||||
"token_type": "bearer",
|
||||
})
|
||||
|
||||
|
||||
# ---------------------- ROTAS DINÂMICAS ----------------------
|
||||
|
||||
@router.get('/', response_model=UserPaginationSchema)
|
||||
def get_users(skip: int = Query(0, ge=0), limit: int = Query(10, ge=1), current_user: dict = Depends(get_current_user)):
|
||||
"""
|
||||
Retorna todos os usuários cadastrados no sistema.
|
||||
"""
|
||||
usuarios = get_all(skip=skip, limit=limit)
|
||||
total = count_users()
|
||||
|
||||
return get_all(skip=skip, limit=limit)
|
||||
|
||||
|
||||
@router.get('/{user_id}', response_model=UserSchemaBase, status_code=status.HTTP_200_OK)
|
||||
def get_user(user_id: int, current_user: dict = Depends(get_current_user)):
|
||||
"""
|
||||
Retorna os dados de um usuário específico pelo ID.
|
||||
"""
|
||||
user = get_user_by_id(user_id)
|
||||
if user:
|
||||
return user
|
||||
|
||||
raise HTTPException(
|
||||
detail='User not found.',
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
|
||||
@router.put('/{user_id}', response_model=UserSchemaBase, status_code=status.HTTP_202_ACCEPTED)
|
||||
def put_user(user_id: int, user: UserSchemaUpdate, current_user: dict = Depends(get_current_user)):
|
||||
"""
|
||||
Atualiza os dados de um usuário específico com os campos fornecidos.
|
||||
"""
|
||||
updated_user = update_user(user_id, user)
|
||||
if updated_user:
|
||||
return updated_user
|
||||
|
||||
raise HTTPException(
|
||||
detail='User not found.',
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
|
||||
@router.delete('/{user_id}', status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_user_by_id(user_id: int):
|
||||
"""
|
||||
Exclui um usuário com base no ID fornecido.
|
||||
"""
|
||||
success = delete_user(user_id)
|
||||
if success:
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
raise HTTPException(
|
||||
detail='User not found.',
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
128
api/v1/models/caixa/c_caixa_item_model.py
Normal file
128
api/v1/models/caixa/c_caixa_item_model.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# models/c_caixa_item_model.py
|
||||
|
||||
from firebird.driver.types import DatabaseError # Importe esta exceção específica
|
||||
from core.database import get_connection
|
||||
# Se você tiver core.configs, pode ser útil para logs ou configurações
|
||||
# from core.configs import settings
|
||||
|
||||
class CCaixaItemModel:
|
||||
"""
|
||||
Classe responsável por interagir diretamente com o banco de dados Firebird.
|
||||
Nenhuma validação ou sanitização deve ser feita aqui.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(caixa_item_id: int) -> dict | None:
|
||||
"""
|
||||
Retorna um usuário com base no ID, ou None se não encontrado.
|
||||
Lança exceções em caso de falha no banco de dados.
|
||||
"""
|
||||
conn = None
|
||||
cur = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("""
|
||||
SELECT CAIXA_ITEM_ID,
|
||||
DESCRICAO,
|
||||
DATA_PAGAMENTO,
|
||||
VALOR_SERVICO,
|
||||
VALOR_PAGO,
|
||||
APRESENTANTE
|
||||
FROM C_CAIXA_ITEM
|
||||
WHERE CAIXA_ITEM_ID = ?
|
||||
""", (caixa_item_id,))
|
||||
row = cur.fetchone()
|
||||
|
||||
if row:
|
||||
return {
|
||||
"caixa_item_id": row[0],
|
||||
"descricao": row[1],
|
||||
"data_pagamento": row[2],
|
||||
"valor_servico": row[3],
|
||||
"valor_pago": row[4],
|
||||
"apresentante": row[5],
|
||||
}
|
||||
return None
|
||||
except DatabaseError as e:
|
||||
print(f"Database error in get_by_id: {e}")
|
||||
raise RuntimeError(f"Erro ao buscar item por ID no banco de dados: {e}")
|
||||
except Exception as e:
|
||||
print(f"Unexpected error in get_by_id: {e}")
|
||||
raise RuntimeError(f"Erro inesperado ao buscar item por ID: {e}")
|
||||
finally:
|
||||
if cur:
|
||||
cur.close()
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
@staticmethod
|
||||
def count_items() -> int:
|
||||
"""
|
||||
Retorna a quantidade de usuários.
|
||||
"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT COUNT(*) FROM C_CAIXA_ITEM")
|
||||
total = cur.fetchone()[0]
|
||||
return total
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Erro ao contar itens: {e}")
|
||||
finally:
|
||||
if cur:
|
||||
cur.close()
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_all_caixa_itens(skip: int = 0, limit: int = 10) -> list[dict]:
|
||||
"""
|
||||
Retorna todos os itens cadastrados no banco de dados.
|
||||
Lança exceções em caso de falha no banco de dados.
|
||||
"""
|
||||
conn = None
|
||||
cur = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
query = f"""
|
||||
SELECT FIRST {limit} SKIP {skip}
|
||||
CAIXA_ITEM_ID,
|
||||
DESCRICAO,
|
||||
DATA_PAGAMENTO,
|
||||
VALOR_SERVICO,
|
||||
VALOR_PAGO,
|
||||
APRESENTANTE
|
||||
FROM C_CAIXA_ITEM
|
||||
ORDER BY CAIXA_ITEM_ID
|
||||
"""
|
||||
|
||||
cur.execute(query)
|
||||
rows = cur.fetchall()
|
||||
|
||||
return [
|
||||
{
|
||||
"caixa_item_id": r[0],
|
||||
"descricao": r[1],
|
||||
"data_pagamento": r[2],
|
||||
"valor_servico": r[3],
|
||||
"valor_pago": r[4],
|
||||
"apresentante": r[5],
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
except DatabaseError as e:
|
||||
print(f"Database error in get_all: {e}")
|
||||
raise RuntimeError(f"Erro ao buscar todos os itens no banco de dados: {e}")
|
||||
except Exception as e:
|
||||
print(f"Unexpected error in get_all: {e}")
|
||||
raise RuntimeError(f"Erro inesperado ao buscar todos os itens: {e}")
|
||||
finally:
|
||||
if cur:
|
||||
cur.close()
|
||||
if conn:
|
||||
conn.close()
|
||||
394
api/v1/models/g_usuario_model.py
Normal file
394
api/v1/models/g_usuario_model.py
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
# models/g_usuario_model.py
|
||||
|
||||
from firebird.driver.types import DatabaseError # Importe esta exceção específica
|
||||
from core.database import get_connection
|
||||
# Se você tiver core.configs, pode ser útil para logs ou configurações
|
||||
# from core.configs import settings
|
||||
|
||||
class UserModel:
|
||||
"""
|
||||
Classe responsável por interagir diretamente com o banco de dados Firebird.
|
||||
Nenhuma validação ou sanitização deve ser feita aqui.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_by_email(email: str) -> dict | None:
|
||||
"""
|
||||
Retorna um usuário com base no e-mail, ou None se não encontrado.
|
||||
Lança exceções em caso de falha no banco de dados.
|
||||
"""
|
||||
conn = None
|
||||
cur = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("""
|
||||
SELECT USUARIO_ID, EMAIL, SENHA_API, NOME_COMPLETO
|
||||
FROM G_USUARIO
|
||||
WHERE EMAIL = ?
|
||||
""", (email,))
|
||||
row = cur.fetchone()
|
||||
|
||||
if row:
|
||||
return {
|
||||
"user_id": row[0],
|
||||
"email": row[1],
|
||||
"senha_api": row[2],
|
||||
"nome_completo": row[3],
|
||||
}
|
||||
return None
|
||||
except DatabaseError as e:
|
||||
# Erros específicos do Firebird (ex: problema na conexão, query inválida)
|
||||
print(f"Database error in get_by_email: {e}")
|
||||
raise RuntimeError(f"Erro ao buscar usuário por e-mail no banco de dados: {e}")
|
||||
except Exception as e:
|
||||
# Qualquer outro erro inesperado
|
||||
print(f"Unexpected error in get_by_email: {e}")
|
||||
raise RuntimeError(f"Erro inesperado ao buscar usuário por e-mail: {e}")
|
||||
finally:
|
||||
if cur:
|
||||
cur.close()
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(user_id: int) -> dict | None:
|
||||
"""
|
||||
Retorna um usuário com base no ID, ou None se não encontrado.
|
||||
Lança exceções em caso de falha no banco de dados.
|
||||
"""
|
||||
conn = None
|
||||
cur = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("""
|
||||
SELECT USUARIO_ID,
|
||||
NOME_COMPLETO,
|
||||
EMAIL,
|
||||
TELEFONE
|
||||
FROM G_USUARIO
|
||||
WHERE USUARIO_ID = ?
|
||||
""", (user_id,))
|
||||
row = cur.fetchone()
|
||||
|
||||
if row:
|
||||
return {
|
||||
"user_id": row[0],
|
||||
"nome_completo": row[1],
|
||||
"email": row[2],
|
||||
"telefone": row[3],
|
||||
}
|
||||
return None
|
||||
except DatabaseError as e:
|
||||
print(f"Database error in get_by_id: {e}")
|
||||
raise RuntimeError(f"Erro ao buscar usuário por ID no banco de dados: {e}")
|
||||
except Exception as e:
|
||||
print(f"Unexpected error in get_by_id: {e}")
|
||||
raise RuntimeError(f"Erro inesperado ao buscar usuário por ID: {e}")
|
||||
finally:
|
||||
if cur:
|
||||
cur.close()
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
@staticmethod
|
||||
def count_users() -> int:
|
||||
"""
|
||||
Retorna a quantidade de usuários.
|
||||
"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT COUNT(*) FROM G_USUARIO")
|
||||
total = cur.fetchone()[0]
|
||||
return total
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Erro ao contar usuários: {e}")
|
||||
finally:
|
||||
if cur:
|
||||
cur.close()
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_all(skip: int = 0, limit: int = 10) -> list[dict]:
|
||||
"""
|
||||
Retorna todos os usuários cadastrados no banco de dados.
|
||||
Lança exceções em caso de falha no banco de dados.
|
||||
"""
|
||||
conn = None
|
||||
cur = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
query = f"""
|
||||
SELECT FIRST {limit} SKIP {skip}
|
||||
USUARIO_ID,
|
||||
TROCARSENHA,
|
||||
LOGIN,
|
||||
SITUACAO,
|
||||
NOME_COMPLETO,
|
||||
FUNCAO,
|
||||
ASSINA,
|
||||
SIGLA,
|
||||
USUARIO_TAB,
|
||||
ULTIMO_LOGIN,
|
||||
ULTIMO_LOGIN_REGS,
|
||||
DATA_EXPIRACAO,
|
||||
ANDAMENTO_PADRAO,
|
||||
LEMBRETE_PERGUNTA,
|
||||
LEMBRETE_RESPOSTA,
|
||||
ANDAMENTO_PADRAO2,
|
||||
RECEBER_MENSAGEM_ARROLAMENTO,
|
||||
EMAIL,
|
||||
ASSINA_CERTIDAO,
|
||||
RECEBER_EMAIL_PENHORA,
|
||||
FOTO,
|
||||
NAO_RECEBER_CHAT_TODOS,
|
||||
PODE_ALTERAR_CAIXA,
|
||||
RECEBER_CHAT_CERTIDAO_ONLINE,
|
||||
RECEBER_CHAT_CANCELAMENTO,
|
||||
CPF,
|
||||
SOMENTE_LEITURA,
|
||||
RECEBER_CHAT_ENVIO_ONR,
|
||||
TIPO_USUARIO,
|
||||
DATA_CADASTRO,
|
||||
TELEFONE
|
||||
FROM G_USUARIO
|
||||
ORDER BY USUARIO_ID
|
||||
"""
|
||||
|
||||
cur.execute(query)
|
||||
rows = cur.fetchall()
|
||||
|
||||
return [
|
||||
{
|
||||
"usuario_id": r[0],
|
||||
"trocarsenha": r[1],
|
||||
"login": r[2],
|
||||
"situacao": r[3],
|
||||
"nome_completo": r[4],
|
||||
"funcao": r[5],
|
||||
"assina": r[6],
|
||||
"sigla": r[7],
|
||||
"usuario_tab": r[8],
|
||||
"ultimo_login": r[9],
|
||||
"ultimo_login_regs": r[10],
|
||||
"data_expiracao": r[11],
|
||||
"andamento_padrao": r[12],
|
||||
"lembrete_pergunta": r[13],
|
||||
"lembrete_resposta": r[14],
|
||||
"andamento_padrao2": r[15],
|
||||
"receber_mensagem_arrolamento": r[16],
|
||||
"email": r[17],
|
||||
"assina_certidao": r[18],
|
||||
"receber_email_penhora": r[19],
|
||||
"foto": r[20],
|
||||
"nao_receber_chat_todos": r[21],
|
||||
"pode_alterar_caixa": r[22],
|
||||
"receber_chat_certidao_online": r[23],
|
||||
"receber_chat_cancelamento": r[24],
|
||||
"cpf": r[25],
|
||||
"somente_leitura": r[26],
|
||||
"receber_chat_envio_onr": r[27],
|
||||
"tipo_usuario": r[28],
|
||||
"data_cadastro": r[29],
|
||||
"telefone": r[30],
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
except DatabaseError as e:
|
||||
print(f"Database error in get_all: {e}")
|
||||
raise RuntimeError(f"Erro ao buscar todos os usuários no banco de dados: {e}")
|
||||
except Exception as e:
|
||||
print(f"Unexpected error in get_all: {e}")
|
||||
raise RuntimeError(f"Erro inesperado ao buscar todos os usuários: {e}")
|
||||
finally:
|
||||
if cur:
|
||||
cur.close()
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
@staticmethod
|
||||
def create(nome_completo: str, email: str, senha_api: str) -> dict | None:
|
||||
"""
|
||||
Cria um novo usuário no banco. Lança exceções para e-mail duplicado
|
||||
ou outros erros de banco de dados.
|
||||
"""
|
||||
conn = None
|
||||
cur = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# A validação de e-mail duplicado feita aqui no modelo é um pouco redundante
|
||||
# se você já tiver uma UNIQUE constraint no EMAIL.
|
||||
# O tratamento de exceção abaixo (DatabaseError) já capturaria a violação da UNIQUE constraint.
|
||||
# No entanto, se você não tiver uma constraint UNIQUE, esta verificação é válida.
|
||||
# Se tiver uma constraint, pode remover este SELECT e deixar o DB lançar o erro.
|
||||
cur.execute("SELECT 1 FROM G_USUARIO WHERE EMAIL = ?", (email,))
|
||||
if cur.fetchone():
|
||||
# Captura de um e-mail já existente antes de tentar a inserção
|
||||
# Isso evita um DatabaseError mais tarde e permite uma mensagem mais específica.
|
||||
raise ValueError("E-mail já cadastrado. Por favor, use outro e-mail.")
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO G_USUARIO (NOME_COMPLETO, EMAIL, SENHA_API)
|
||||
VALUES (?, ?, ?)
|
||||
RETURNING USUARIO_ID
|
||||
""", (nome_completo, email, senha_api))
|
||||
|
||||
user_id = cur.fetchone()[0]
|
||||
conn.commit()
|
||||
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"nome_completo": nome_completo,
|
||||
"email": email,
|
||||
}
|
||||
except DatabaseError as e:
|
||||
if conn:
|
||||
conn.rollback() # Desfaz a transação em caso de erro no DB
|
||||
error_message = str(e).lower()
|
||||
# Tratamento específico para violação de UNIQUE KEY ou PRIMARY KEY
|
||||
if "violation of primary or unique key constraint" in error_message or "duplicate value" in error_message:
|
||||
# Se você tem certeza de que USUARIO_ID e EMAIL são as únicas chaves únicas/primárias
|
||||
# e o EMAIL já foi verificado acima, então este erro aponta para USUARIO_ID.
|
||||
# No entanto, é mais robusto verificar o nome da constraint se possível, ou ser mais genérico.
|
||||
if "g_usuario_pk" in error_message: # Supondo que G_USUARIO_PK é para USUARIO_ID
|
||||
raise ValueError(f"O ID gerado para o usuário já existe. Tente novamente ou verifique os dados: {error_message}")
|
||||
# Se você tiver uma constraint UNIQUE no EMAIL, isso também seria capturado aqui
|
||||
elif "seu_email_unique_constraint_name" in error_message: # Substitua pelo nome real da sua constraint de email
|
||||
raise ValueError(f"Já existe um usuário com este e-mail: {email}. Erro: {error_message}")
|
||||
else: # Outras violações de chaves únicas
|
||||
raise ValueError(f"Violação de restrição de dados. Verifique os campos fornecidos. Detalhe: {error_message}")
|
||||
else:
|
||||
# Outros erros de DatabaseError
|
||||
raise RuntimeError(f"Erro no banco de dados ao criar usuário: {error_message}")
|
||||
except ValueError as e:
|
||||
# Re-lança o ValueError de e-mail duplicado, se a verificação acima estiver ativa
|
||||
raise e
|
||||
except Exception as e:
|
||||
if conn:
|
||||
conn.rollback()
|
||||
print(f"Unexpected error in create: {e}")
|
||||
raise RuntimeError(f"Erro inesperado ao criar usuário: {e}")
|
||||
finally:
|
||||
if cur:
|
||||
cur.close()
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
@staticmethod
|
||||
def update(user_id: int, nome_completo: str | None, email: str | None, senha_api: str | None, telefone: str | None) -> bool:
|
||||
"""
|
||||
Atualiza os dados de um usuário existente.
|
||||
Lança exceções se o usuário não for encontrado ou em caso de erro no DB.
|
||||
"""
|
||||
conn = None
|
||||
cur = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Verifica se o usuário existe antes de tentar atualizar
|
||||
cur.execute("SELECT 1 FROM G_USUARIO WHERE USUARIO_ID = ?", (user_id,))
|
||||
if not cur.fetchone():
|
||||
raise KeyError(f"Usuário com ID {user_id} não encontrado para atualização.")
|
||||
|
||||
updates = []
|
||||
params = []
|
||||
|
||||
if nome_completo is not None:
|
||||
updates.append("NOME_COMPLETO = ?")
|
||||
params.append(nome_completo)
|
||||
if email is not None:
|
||||
# Verifique se o novo e-mail já existe para outro usuário (se email for uma chave única)
|
||||
cur.execute("SELECT USUARIO_ID FROM G_USUARIO WHERE EMAIL = ? AND USUARIO_ID <> ?", (email, user_id))
|
||||
if cur.fetchone():
|
||||
raise ValueError(f"O e-mail '{email}' já está sendo usado por outro usuário.")
|
||||
updates.append("EMAIL = ?")
|
||||
params.append(email)
|
||||
if senha_api is not None:
|
||||
updates.append("SENHA_API = ?")
|
||||
params.append(senha_api)
|
||||
if telefone is not None:
|
||||
updates.append("TELEFONE = ?")
|
||||
params.append(telefone)
|
||||
|
||||
if not updates: # Se nenhum campo foi fornecido para atualização
|
||||
return False
|
||||
|
||||
query = f"UPDATE G_USUARIO SET {', '.join(updates)} WHERE USUARIO_ID = ?"
|
||||
params.append(user_id)
|
||||
|
||||
cur.execute(query, tuple(params))
|
||||
conn.commit()
|
||||
|
||||
return True
|
||||
except DatabaseError as e:
|
||||
if conn:
|
||||
conn.rollback()
|
||||
error_message = str(e).lower()
|
||||
if "violation of unique constraint" in error_message or "duplicate value" in error_message:
|
||||
# Captura violação de unique constraint no email, se houver
|
||||
raise ValueError(f"Erro de dados duplicados ao atualizar: {error_message}")
|
||||
else:
|
||||
raise RuntimeError(f"Erro no banco de dados ao atualizar usuário: {e}")
|
||||
except (KeyError, ValueError) as e:
|
||||
# Re-lança as exceções de não encontrado ou e-mail duplicado
|
||||
raise e
|
||||
except Exception as e:
|
||||
if conn:
|
||||
conn.rollback()
|
||||
print(f"Unexpected error in update: {e}")
|
||||
raise RuntimeError(f"Erro inesperado ao atualizar usuário: {e}")
|
||||
finally:
|
||||
if cur:
|
||||
cur.close()
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
@staticmethod
|
||||
def delete(user_id: int) -> bool:
|
||||
"""
|
||||
Exclui um usuário com base no ID. Lança exceção se o usuário não for encontrado.
|
||||
"""
|
||||
conn = None
|
||||
cur = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Verifica se o usuário existe antes de tentar deletar
|
||||
cur.execute("SELECT 1 FROM G_USUARIO WHERE USUARIO_ID = ?", (user_id,))
|
||||
if not cur.fetchone():
|
||||
raise KeyError(f"Usuário com ID {user_id} não encontrado para exclusão.")
|
||||
|
||||
cur.execute("DELETE FROM G_USUARIO WHERE USUARIO_ID = ?", (user_id,))
|
||||
conn.commit()
|
||||
|
||||
return True
|
||||
except DatabaseError as e:
|
||||
if conn:
|
||||
conn.rollback()
|
||||
print(f"Database error in delete: {e}")
|
||||
raise RuntimeError(f"Erro no banco de dados ao excluir usuário: {e}")
|
||||
except KeyError as e:
|
||||
# Re-lança a exceção de não encontrado
|
||||
raise e
|
||||
except Exception as e:
|
||||
if conn:
|
||||
conn.rollback()
|
||||
print(f"Unexpected error in delete: {e}")
|
||||
raise RuntimeError(f"Erro inesperado ao excluir usuário: {e}")
|
||||
finally:
|
||||
if cur:
|
||||
cur.close()
|
||||
if conn:
|
||||
conn.close()
|
||||
34
api/v1/schemas/caixa/c_caixa_item_schema.py
Normal file
34
api/v1/schemas/caixa/c_caixa_item_schema.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# schemas/c_caixa_item_schema.py
|
||||
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
# Schema base usado para representar um caixa_item retornado pela API
|
||||
class CCaixaItemSchemaBase(BaseModel):
|
||||
caixa_item_id: Optional[int] = None
|
||||
descricao: Optional[str] = None
|
||||
data_pagamento: Optional[datetime] = None
|
||||
valor_servico: Optional[Decimal] = None
|
||||
valor_pago: Optional[Decimal] = None
|
||||
apresentante: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True # Permite construir a partir de dicts ou ORMs (mesmo sem ORM aqui)
|
||||
|
||||
# Schema para listagem de registros com paginação
|
||||
class CCaixaItemSchemaList(BaseModel):
|
||||
caixa_item_id: Optional[int]
|
||||
descricao: Optional[str]
|
||||
data_pagamento: Optional[datetime]
|
||||
valor_servico: Optional[Decimal]
|
||||
valor_pago: Optional[Decimal]
|
||||
apresentante: Optional[str]
|
||||
|
||||
|
||||
class CCaixaItemPaginationSchema(BaseModel):
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
data: List[CCaixaItemSchemaList]
|
||||
73
api/v1/schemas/caixa/t_ato_schema.py
Normal file
73
api/v1/schemas/caixa/t_ato_schema.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# schemas/t_ato_schema.py
|
||||
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from datetime import datetime
|
||||
|
||||
# Schema base usado para representar um usuário retornado pela API
|
||||
class TAtoSchemaBase(BaseModel):
|
||||
user_id: Optional[int] = None # ID do usuário (opcional)
|
||||
nome_completo: Optional[str] = None # Nome completo do usuário
|
||||
email: Optional[EmailStr] = None # E-mail validado do usuário
|
||||
|
||||
class Config:
|
||||
from_attributes = True # Permite construir a partir de dicts ou ORMs (mesmo sem ORM aqui)
|
||||
|
||||
|
||||
# Schema usado para criação de um novo usuário
|
||||
class TAtoSchemaCreate(BaseModel):
|
||||
nome_completo: str # Nome completo obrigatório
|
||||
email: EmailStr # E-mail obrigatório e validado
|
||||
senha_api: str # Senha enviada pelo cliente (será criptografada no backend)
|
||||
|
||||
|
||||
# Schema usado para atualização de dados do usuário
|
||||
# Todos os campos são opcionais para permitir atualizações parciais
|
||||
class TAtoSchemaUpdate(BaseModel):
|
||||
nome_completo: Optional[str] = None # Atualização do nome, se fornecido
|
||||
email: Optional[EmailStr] = None # Atualização do e-mail, se fornecido
|
||||
senha_api: Optional[str] = None # Atualização da senha, se fornecida
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Schema para listagem de registros com paginação
|
||||
class TAtoSchemaList(BaseModel):
|
||||
usuario_id: float # USUARIO_ID NUMERIC(10,2)
|
||||
trocarsenha: Optional[str]
|
||||
login: Optional[str]
|
||||
situacao: Optional[str]
|
||||
nome_completo: Optional[str]
|
||||
funcao: Optional[str]
|
||||
assina: Optional[str]
|
||||
sigla: Optional[str]
|
||||
usuario_tab: Optional[float] # NUMERIC(10,2)
|
||||
ultimo_login: Optional[datetime]
|
||||
ultimo_login_regs: Optional[datetime]
|
||||
data_expiracao: Optional[datetime]
|
||||
andamento_padrao: Optional[float] # NUMERIC(10,2)
|
||||
lembrete_pergunta: Optional[str]
|
||||
lembrete_resposta: Optional[str]
|
||||
andamento_padrao2: Optional[float] # NUMERIC(10,2)
|
||||
receber_mensagem_arrolamento: Optional[str]
|
||||
email: Optional[EmailStr] # ou Optional[str] se preferir mais flexível
|
||||
assina_certidao: Optional[str]
|
||||
receber_email_penhora: Optional[str]
|
||||
foto: Optional[bytes] # BLOB SUB_TYPE BINARY
|
||||
nao_receber_chat_todos: Optional[str]
|
||||
pode_alterar_caixa: Optional[str]
|
||||
receber_chat_certidao_online: Optional[str]
|
||||
receber_chat_cancelamento: Optional[str]
|
||||
cpf: Optional[str]
|
||||
somente_leitura: Optional[str]
|
||||
receber_chat_envio_onr: Optional[str]
|
||||
tipo_usuario: Optional[str]
|
||||
data_cadastro: Optional[datetime]
|
||||
|
||||
|
||||
class TAtoPaginationSchema(BaseModel):
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
data: List[TAtoSchemaList]
|
||||
76
api/v1/schemas/g_usuario_schema.py
Normal file
76
api/v1/schemas/g_usuario_schema.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# schemas/g_usuario_schema.py
|
||||
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from datetime import datetime
|
||||
|
||||
# Schema base usado para representar um usuário retornado pela API
|
||||
class UserSchemaBase(BaseModel):
|
||||
user_id: Optional[int] = None # ID do usuário (opcional)
|
||||
nome_completo: Optional[str] = None # Nome completo do usuário
|
||||
email: Optional[EmailStr] = None # E-mail validado do usuário
|
||||
telefone: Optional[str] = None # Telefone validado do usuário
|
||||
|
||||
class Config:
|
||||
from_attributes = True # Permite construir a partir de dicts ou ORMs (mesmo sem ORM aqui)
|
||||
|
||||
|
||||
# Schema usado para criação de um novo usuário
|
||||
class UserSchemaCreate(BaseModel):
|
||||
nome_completo: str # Nome completo obrigatório
|
||||
email: EmailStr # E-mail obrigatório e validado
|
||||
senha_api: str # Senha enviada pelo cliente (será criptografada no backend)
|
||||
|
||||
|
||||
# Schema usado para atualização de dados do usuário
|
||||
# Todos os campos são opcionais para permitir atualizações parciais
|
||||
class UserSchemaUpdate(BaseModel):
|
||||
nome_completo: Optional[str] = None # Atualização do nome, se fornecido
|
||||
email: Optional[EmailStr] = None # Atualização do e-mail, se fornecido
|
||||
senha_api: Optional[str] = None # Atualização da senha, se fornecida
|
||||
telefone: Optional[str] = None # Atualização do telefone, se fornecida
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Schema para listagem de registros com paginação
|
||||
class UserSchemaList(BaseModel):
|
||||
usuario_id: float # USUARIO_ID NUMERIC(10,2)
|
||||
trocarsenha: Optional[str]
|
||||
login: Optional[str]
|
||||
situacao: Optional[str]
|
||||
nome_completo: Optional[str]
|
||||
funcao: Optional[str]
|
||||
assina: Optional[str]
|
||||
sigla: Optional[str]
|
||||
usuario_tab: Optional[float] # NUMERIC(10,2)
|
||||
ultimo_login: Optional[datetime]
|
||||
ultimo_login_regs: Optional[datetime]
|
||||
data_expiracao: Optional[datetime]
|
||||
andamento_padrao: Optional[float] # NUMERIC(10,2)
|
||||
lembrete_pergunta: Optional[str]
|
||||
lembrete_resposta: Optional[str]
|
||||
andamento_padrao2: Optional[float] # NUMERIC(10,2)
|
||||
receber_mensagem_arrolamento: Optional[str]
|
||||
email: Optional[EmailStr] # ou Optional[str] se preferir mais flexível
|
||||
assina_certidao: Optional[str]
|
||||
receber_email_penhora: Optional[str]
|
||||
foto: Optional[bytes] # BLOB SUB_TYPE BINARY
|
||||
nao_receber_chat_todos: Optional[str]
|
||||
pode_alterar_caixa: Optional[str]
|
||||
receber_chat_certidao_online: Optional[str]
|
||||
receber_chat_cancelamento: Optional[str]
|
||||
cpf: Optional[str]
|
||||
somente_leitura: Optional[str]
|
||||
receber_chat_envio_onr: Optional[str]
|
||||
tipo_usuario: Optional[str]
|
||||
data_cadastro: Optional[datetime]
|
||||
telefone: Optional[str]
|
||||
|
||||
|
||||
class UserPaginationSchema(BaseModel):
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
data: List[UserSchemaList]
|
||||
35
core/auth.py
Normal file
35
core/auth.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
from pytz import timezone
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import jwt
|
||||
|
||||
from core.configs import settings
|
||||
from api.v1.controllers.g_usuario_controller import authenticate_user
|
||||
|
||||
# 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
|
||||
)
|
||||
34
core/configs.py
Normal file
34
core/configs.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from pydantic_settings import BaseSettings
|
||||
|
||||
# Classe de configurações gerais da aplicação
|
||||
class Settings(BaseSettings):
|
||||
# Prefixo padrão para as rotas da API
|
||||
API_V1_STR: str = '/api/v1'
|
||||
|
||||
# URL de conexão com o banco de dados Firebird 4 (driver oficial)
|
||||
# Obs: encode a senha corretamente se houver caracteres especiais
|
||||
# DB_URL: str = "firebird://SYSDBA:Sun147oi.@185.139.1.35:3050/CARTORIO"
|
||||
DB_URL: str = "firebird://SYSDBA:Sun147oi.@185.139.1.35:3050/CARTORIO"
|
||||
|
||||
|
||||
# Chave secreta usada para geração de tokens JWT
|
||||
JWT_SECRET: str = 'WYe1zwtlDkh39_X3X3qTSICFDxts4VQrMyGLxnEpGUg'
|
||||
|
||||
"""
|
||||
Para gerar uma nova chave JWT segura, use:
|
||||
import secrets
|
||||
secrets.token_urlsafe(32)
|
||||
"""
|
||||
|
||||
# Algoritmo usado para assinar os tokens JWT
|
||||
ALGORITHM: str = 'HS256'
|
||||
|
||||
# Tempo de expiração do token JWT (em minutos): 1 semana
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7
|
||||
|
||||
# Configuração do Pydantic
|
||||
class Config:
|
||||
case_sensitive = True # Variáveis de ambiente sensíveis a maiúsculas/minúsculas
|
||||
|
||||
# Instância global das configurações
|
||||
settings: Settings = Settings()
|
||||
28
core/database.py
Normal file
28
core/database.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import os
|
||||
from urllib.parse import urlparse, unquote
|
||||
from firebird.driver import connect
|
||||
from core.configs import settings
|
||||
|
||||
def get_connection():
|
||||
parsed = urlparse(settings.DB_URL)
|
||||
|
||||
user = parsed.username
|
||||
password = unquote(parsed.password or '')
|
||||
host = parsed.hostname
|
||||
port = parsed.port or 3050
|
||||
database = parsed.path.lstrip('/')
|
||||
|
||||
# Ajusta o caminho no Windows: transforma '/' em '\'
|
||||
if os.name == 'nt': # 'nt' = Windows
|
||||
database = database.replace('/', '\\')
|
||||
|
||||
# Constrói o DSN no formato 'hostname/port:database_path'
|
||||
# E essa string é passada como o PRIMEIRO ARGUMENTO POSICIONAL
|
||||
connection_dsn = f"{host}/{port}:{database}"
|
||||
|
||||
return connect(
|
||||
connection_dsn, # Este é o DSN completo que o driver espera
|
||||
user=user,
|
||||
password=password,
|
||||
charset="UTF8"
|
||||
)
|
||||
64
core/deps.py
Normal file
64
core/deps.py
Normal file
|
|
@ -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.models.g_usuario_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
|
||||
32
core/security.py
Normal file
32
core/security.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# 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')
|
||||
|
||||
|
||||
# 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:
|
||||
"""
|
||||
Gera e retorna o hash da senha fornecida.
|
||||
|
||||
:param plain_senha_api: Senha em texto puro fornecida pelo usuário
|
||||
:return: Hash da senha
|
||||
"""
|
||||
return CRYPTO.hash(plain_senha_api)
|
||||
32
core/validation.py
Normal file
32
core/validation.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# 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 "<script" in text.lower() or "javascript:" in text.lower()
|
||||
|
||||
@staticmethod
|
||||
def is_safe(text: str) -> bool:
|
||||
"""Detect common XSS/SQL injection characters or patterns"""
|
||||
blacklist = ["<script", "javascript:", "--", ";", "/*", "*/", "@@", "char(", "nchar(", "varchar(", "alter", "drop", "exec"]
|
||||
text_lower = text.lower()
|
||||
return not any(p in text_lower for p in blacklist)
|
||||
34
main.py
Normal file
34
main.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# main.py
|
||||
|
||||
# Ajuste para garantir que o diretório base do projeto seja incluído no PYTHONPATH
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Adiciona o diretório atual (onde está o main.py) ao sys.path
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Importa a classe principal do FastAPI
|
||||
from fastapi import FastAPI
|
||||
|
||||
# Importa as configurações globais da aplicação
|
||||
from core.configs import settings
|
||||
|
||||
# Importa o roteador principal da API versão 1
|
||||
from api.v1.api import api_router
|
||||
|
||||
# Instancia o app FastAPI com um título personalizado
|
||||
app = FastAPI(title='Orius Cartórios')
|
||||
|
||||
# Inclui as rotas da versão 1 da API com prefixo definido em settings (ex: /api/v1)
|
||||
app.include_router(api_router, prefix=settings.API_V1_STR)
|
||||
|
||||
# Executa o servidor com Uvicorn se este arquivo for executado diretamente
|
||||
if __name__ == '__main__':
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
"main:app", # Caminho do app para execução
|
||||
host="0.0.0.0", # Disponibiliza a aplicação externamente
|
||||
port=8000, # Porta padrão
|
||||
log_level='info', # Define o nível de log para desenvolvimento
|
||||
reload=True # Ativa auto-reload durante desenvolvimento
|
||||
)
|
||||
1
query
Normal file
1
query
Normal file
|
|
@ -0,0 +1 @@
|
|||
FirebirdServerDefaultInstance
|
||||
39
requirements.txt
Normal file
39
requirements.txt
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
annotated-types==0.7.0
|
||||
anyio==4.9.0
|
||||
asyncpg==0.30.0
|
||||
bcrypt==3.2.0
|
||||
cffi==1.17.1
|
||||
click==8.2.1
|
||||
colorama==0.4.6
|
||||
cryptography==45.0.2
|
||||
dnspython==2.7.0
|
||||
ecdsa==0.19.1
|
||||
email_validator==2.2.0
|
||||
fastapi==0.115.12
|
||||
firebird-base==2.0.0
|
||||
firebird-driver==2.0.2
|
||||
future==1.0.0
|
||||
greenlet==3.2.2
|
||||
h11==0.16.0
|
||||
idna==3.10
|
||||
packaging==25.0
|
||||
passlib==1.7.4
|
||||
protobuf==5.29.4
|
||||
pyasn1==0.4.8
|
||||
pycparser==2.22
|
||||
pydantic==2.11.4
|
||||
pydantic-settings==2.9.1
|
||||
pydantic_core==2.33.2
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dotenv==1.1.0
|
||||
python-jose==3.4.0
|
||||
python-multipart==0.0.20
|
||||
pytz==2025.2
|
||||
rsa==4.9.1
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
sqlalchemy-firebird==2.1
|
||||
starlette==0.46.2
|
||||
typing-inspection==0.4.0
|
||||
typing_extensions==4.13.2
|
||||
uvicorn==0.34.2
|
||||
8
services/api_externa_1.py
Normal file
8
services/api_externa_1.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import httpx
|
||||
|
||||
# Exemplo de cliente HTTP para API externa
|
||||
async def buscar_dados_externos(param: str):
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
response = await client.get(f"https://api.exemplo.com/{param}")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
Reference in a new issue