- inits centralizados, READMEs atualizados

- padronizando o nome de get_db_connection e session para get_db_session, para não confundir com session do Flask ou sessoes web

- corrigindo potenciais erros

-- has_permission nao consegue com lazy load carregar permission depois de load_user fechar a conexao, entao joinedLoad com Permission antes de fechar

-- db.rollback não existe caso db = get_db_session() apareça muito depois dentro do try, padronizando antes de try

--- comparar role por nivel (Role.SECRETARIO_GERAL) e nao por nome ("Secretario Geral")

- unificacao de get_otp_qr_code

- mudança de nowutc() para now(UTC) conforme novo padrão
This commit is contained in:
2026-02-20 17:19:15 -03:00
parent 6882b57081
commit 2b1668206d
38 changed files with 1250 additions and 1187 deletions

View File

@@ -1,48 +1,48 @@
FROM alpine:latest FROM alpine:latest
# Instalar dependências do sistema # Diretório de trabalho
RUN apk update && \
apk add --no-cache \
python3 \
py3-pip \
make \
git \
gcc \
python3-dev \
musl-dev \
linux-headers
# Criar link simbólico para python3
RUN ln -sf python3 /usr/bin/python
# Definir diretório de trabalho
WORKDIR /app WORKDIR /app
# Copiar arquivos do projeto # UID/GID configuráveis para compatibilizar permissões em bind mounts Linux
ARG APP_UID=1000
ARG APP_GID=1000
# Instalar Python no Alpine e criar alias `python`
RUN apk add --no-cache python3 py3-pip \
&& ln -sf python3 /usr/bin/python
# Instalar dependências Python em venv usando build deps temporários
COPY requirements.txt .
RUN apk add --no-cache --virtual .build-deps \
gcc \
musl-dev \
linux-headers \
&& python -m venv /venv \
&& /venv/bin/pip install --upgrade pip \
&& /venv/bin/pip install --no-cache-dir -r requirements.txt \
&& apk del .build-deps
# Copiar código da aplicação
COPY . . COPY . .
# Criar e ativar ambiente virtual # Criar usuário sem privilégios e diretórios de escrita necessários
RUN python -m venv /venv && \ RUN addgroup -S -g "${APP_GID}" appgroup \
. /venv/bin/activate && \ && adduser -S -D -H -u "${APP_UID}" -G appgroup appuser \
pip install --upgrade pip && \ && mkdir -p /data /app/logs \
pip install -r requirements.txt && chown -R appuser:appgroup /app /data /venv
# Ambiente padrão
ENV PATH="/venv/bin:$PATH" \
FLASK_APP=app.py \
FLASK_ENV=production \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Rodar aplicação como usuário não-root
USER appuser
# Expor a porta que o Flask usa # Expor a porta que o Flask usa
EXPOSE 5000 EXPOSE 5000
# Definir o ambiente virtual como padrão
ENV PATH="/venv/bin:$PATH"
ENV FLASK_APP=app.py
ENV FLASK_ENV=production
# Criar script de inicialização
RUN echo '#!/bin/sh' > /app/start.sh && \
echo 'echo "Inicializando banco de dados..."' >> /app/start.sh && \
echo 'python init_db.py' >> /app/start.sh && \
echo 'echo "Banco de dados inicializado!"' >> /app/start.sh && \
echo 'echo "Iniciando aplicação..."' >> /app/start.sh && \
echo 'exec gunicorn --bind 0.0.0.0:5000 app:app' >> /app/start.sh && \
chmod +x /app/start.sh
# Comando para rodar a aplicação # Comando para rodar a aplicação
CMD ["/app/start.sh"] CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

112
Makefile
View File

@@ -1,58 +1,126 @@
.PHONY: install clean db-reset db-seed-fake db-seed-test-users admin-reset admin-rotate-otp \
run run-gunicorn docker-db-reset docker-db-seed-fake docker-db-seed-test-users \
docker-admin-reset docker-admin-rotate-otp docker-build docker-up docker-down docker-logs \
docker-restart docker-db-reset-xplat docker-db-seed-fake-xplat docker-db-seed-test-users-xplat \
docker-admin-reset-xplat docker-admin-rotate-otp-xplat docker-build-xplat docker-up-xplat \
docker-down-xplat docker-logs-xplat cache-clear cache-status cache-keys dev-up dev-down \
prod-build prod-up prod-logs cache-warmup cache-monitor
install: install:
pip install -r requirements.txt pip install -r requirements.txt
clean: clean:
rm -rf ~/.local/share/controles/database.db* rm -f ~/.local/share/controles/database.db*
rm -f database.db*
rm -f data/database.db*
rm -f admin_qr.png rm -f admin_qr.png
rm -f data/admin_qr.png
rm -f /tmp/admin_qr.png
init-db: clean db-reset: clean
python init_db.py PYTHONUNBUFFERED=1 python -B scripts/manage.py db_reset
seed: init-db # Apenas seed (seed_database.py)
python seed.py db-seed-fake:
PYTHONUNBUFFERED=1 python -B scripts/manage.py db_seed_fake
init: # Apenas seed (create_test_users.py)
python app.py --init db-seed-test-users:
PYTHONUNBUFFERED=1 python -B scripts/manage.py db_seed_test_users
# Busca o OTP padrão
admin-reset:
PYTHONUNBUFFERED=1 python -B scripts/manage.py admin_reset
# Novo OTP
admin-rotate-otp:
PYTHONUNBUFFERED=1 python -B scripts/manage.py admin_rotate_otp
# Server padrão do python
run: run:
python app.py PYTHONUNBUFFERED=1 python -B app.py
run-with-seed: seed init run # server padrão de produção (recomendado)
run-gunicorn:
reset-admin: clean PYTHONUNBUFFERED=1 python -B -m gunicorn --bind 0.0.0.0:5000 app:app
python create_admin.py
# Docker commands # Docker commands
docker-db-reset:
mkdir -p data logs
docker-compose -f docker-compose.yml exec app python -B scripts/manage.py db_reset
docker-db-seed-fake:
docker-compose -f docker-compose.yml exec app python -B scripts/manage.py db_seed_fake
docker-db-seed-test-users:
docker-compose -f docker-compose.yml exec app python -B scripts/manage.py db_seed_test_users
docker-admin-reset:
docker-compose -f docker-compose.yml exec app python -B scripts/manage.py admin_reset
docker-admin-rotate-otp:
docker-compose -f docker-compose.yml exec app python -B scripts/manage.py admin_rotate_otp
docker-build: docker-build:
docker-compose build mkdir -p data logs
docker-compose -f docker-compose.yml build
docker-up: docker-up:
docker-compose up -d mkdir -p data logs
docker-compose -f docker-compose.yml up -d
docker-down: docker-down:
docker-compose down docker-compose -f docker-compose.yml down
docker-logs: docker-logs:
docker-compose logs -f docker-compose -f docker-compose.yml logs -f
docker-restart: docker-restart:
docker-compose restart docker-compose -f docker-compose.yml restart
# Docker commands (fallback cross-platform)
docker-db-reset-xplat:
docker-compose -f docker-compose.crossplatform.yml exec app python -B scripts/manage.py db_reset
docker-db-seed-fake-xplat:
docker-compose -f docker-compose.crossplatform.yml exec app python -B scripts/manage.py db_seed_fake
docker-db-seed-test-users-xplat:
docker-compose -f docker-compose.crossplatform.yml exec app python -B scripts/manage.py db_seed_test_users
docker-admin-reset-xplat:
docker-compose -f docker-compose.crossplatform.yml exec app python -B scripts/manage.py admin_reset
docker-admin-rotate-otp-xplat:
docker-compose -f docker-compose.crossplatform.yml exec app python -B scripts/manage.py admin_rotate_otp
docker-build-xplat:
mkdir -p data logs
docker-compose -f docker-compose.crossplatform.yml build
docker-up-xplat:
docker-compose -f docker-compose.crossplatform.yml up -d
docker-down-xplat:
docker-compose -f docker-compose.crossplatform.yml down
docker-logs-xplat:
docker-compose -f docker-compose.crossplatform.yml logs -f
# Redis cache commands # Redis cache commands
cache-clear: cache-clear:
docker-compose exec redis redis-cli FLUSHDB docker-compose -f docker-compose.yml exec redis redis-cli FLUSHDB
cache-status: cache-status:
docker-compose exec redis redis-cli INFO docker-compose -f docker-compose.yml exec redis redis-cli INFO
cache-keys: cache-keys:
docker-compose exec redis redis-cli KEYS "*" docker-compose -f docker-compose.yml exec redis redis-cli KEYS "*"
# Development with Docker # Development with Docker
dev-up: docker-build docker-up dev-up: docker-build docker-up
@echo "Development environment started with Redis cache" @echo "Development environment started with Redis cache"
@echo "Application: http://localhost:5000" @echo "Application: http://localhost:5000"
@echo "Redis: localhost:6379"
dev-down: docker-down dev-down: docker-down
@echo "Development environment stopped" @echo "Development environment stopped"
@@ -77,4 +145,4 @@ cache-warmup:
cache-monitor: cache-monitor:
@echo "Monitoring Redis cache..." @echo "Monitoring Redis cache..."
watch -n 5 'docker-compose exec redis redis-cli INFO memory' watch -n 5 'docker-compose -f docker-compose.yml exec redis redis-cli INFO memory'

313
README.md
View File

@@ -1,10 +1,258 @@
# Sistema de Controle de Militantes # Sistema de Controles OCI
Sistema para gerenciamento de militantes, células, setores e comitês regionais. Sistema web para gestão organizacional (militantes, estrutura hierárquica, cotas, pagamentos e materiais), com autenticação por senha + OTP, permissões RBAC e cache Redis.
## 🔧 Tecnologias
- **Backend**: Flask 2.3.3
- **Frontend**: Bootstrap 5, HTML5, CSS3, JavaScript
- **Database**: SQLite + SQLAlchemy 2.0.21
- **Cache**: Redis 7.4.4 (opcional fora do Docker)
- **Authentication**: Flask-Login + OTP (pyotp)
- **Container**: Docker + Docker Compose
- **Server**: Gunicorn
## 🚀 Status Atual
- Sistema com Arquitetura de Permissões (RBAC)
- Sistema de permissões implementado no nível de dados
- Estrutura organizacional completa
- Aplicação Flask rodando com Docker
- Redis cache integrado e funcionando
- Banco de dados SQLite inicializado
- Usuário admin configurado com OTP
- 30 militantes de teste criados
- Menus sempre visíveis, controle transparente
## 🏗️ Arquitetura de Permissões
O sistema implementa uma estratégia de controle de permissões no **nível de dados**, garantindo que:
- **Menus permanecem sempre visíveis** - Não há restrições na interface
- **Dados são filtrados por hierarquia** - Admin → CC → CR → Setor → Célula
- **Templates nunca quebram** - Sempre renderizam, mesmo com dados vazios
## ⚙️ Instalação - Pré-requisitos
- Docker + Docker Compose (para fluxo com containers)
- Porta 5000 disponível para a aplicação
- Porta 6379 disponível para Redis
- Python 3.10+ (recomendado)
- `pip`
- `make`
## 🐳 Primeiro Inicio com Docker (recomendado)
### 0. Clone o repositorio
```bash
git clone git@gitea.comunatec.org:comunatec/controles.git
cd controles
```
### 1. Resete o banco
```bash
make docker-db-reset
```
### 2. Adicione dados fakes para testes (opcional)
```bash
make docker-db-seed-fake
```
### 3. Subir aplicação
```bash
make dev-up
```
### 4. Acompanhar logs
- Aplicação: `logs/controles.log`
- Cache: `logs/cache.log`
- Docker: `docker-compose logs`
```bash
make docker-logs # Toda a aplicação
docker-compose logs redis # Somente o redis
make cache-status # INFO do redis
```
### 5. Descer aplicação
```bash
make dev-down
```
## 🐍 Primeiro Inicio - Execução Local (Sem Docker)
### 0. Clone o repositorio
```bash
git clone git@gitea.comunatec.org:comunatec/controles.git
cd controles
```
### 1. Ambiente Python
```bash
python -m venv .venv
source .venv/bin/activate # Linux/Mac
# ou
venv\Scripts\activate # Windows
pip install -r requirements.txt
```
### 2. Crie o `.env` na raiz
Exemplo:
```env
# Usando OTP padrão para não trocar toda hora no desenvolvimento
ADMIN_OTP_SECRET=JBSWY3DPEHPK3PXP
# Para usar o mesmo banco que o Docker (Linux/WSL permite bind-mount)
DATABASE_URL=sqlite:///data/database.db
REDIS_URL=redis://redis:6379/0
FLASK_APP=app.py
FLASK_ENV=development
SECRET_KEY=troque_esta_chave
APP_UID=1000
APP_GID=1000
MAIL_SERVER=seu_servidor_smtp
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=seu_email
MAIL_PASSWORD=sua_senha
```
Se Redis não estiver disponível localmente, a aplicação continua rodando sem cache.
### 3. Inicialize banco e rode
```bash
make db-reset
make db-seed-fake # opcional
make run
# ou
make run-gunicorn # server de produção
```
## 🔐 Acesso ao Sistema
### Credenciais do Admin
- **URL**: http://localhost:5000
- **Usuário**: admin
- **Senha**: admin123
- **OTP Secret**: JBSWY3DPEHPK3PXP
### Configuração OTP
1. Instale um aplicativo autenticador (Google Authenticator, Microsoft Authenticator)
2. Configure manualmente:
- Descrição da chave (Codinome): Controles-OCI-admin
- Segredo OTP (Sua Chave): JBSWY3DPEHPK3PXP
- Tipo: TOTP
- Algoritmo: SHA1
- Dígitos: 6
- Intervalo: 30 segundos
**OU** use o QR Code gerado em `/tmp/admin_qr.png` ou `/data/admin_qr.png` ou `admin_qr.png`.
PS: Google Authenticator só tem "Codinome" e "Sua Chave" de config, e tá tudo bem.
## Testes Automatizados
```bash
# ambiente já configurado
pip install -r tests/requirements-test.txt
pytest
```
Também existe `run_tests.sh`, que monta um venv e executa a suíte automaticamente.
- TODO: Talvez trocar o nome para venv_test
## 📁 Estrutura de arquivos
O sistema busca seguir padrão MVC (Model-View-Controller), atualmente está:
```
controles/
├── controllers/ # Controladores (lógica de rotas)
├── data/ # Banco de dados (e talvez qr_code admin)
├── docs/ # Documentações da arquitetura
├── functions/ # Funções utilitárias
├── logs/ # Logs d aplicação, redis...
├── migrations/ # Alterações de banco para não perder dados (produção)
├── models/ # Modelos (operações de banco)
├── routes/ # Rotas de aplicação
├── scripts/ # Scripts de gerenciamento
├── services/ # Serviços (lógica de negócio)
├── sql/ # Migrate para o rbac
├── static/ # Arquivos estáticos (icon/css/js)
├── templates/ # Views (templates HTML)
├── tests/ # Testes automatizados
├── utils/ # Funções sem regra de negócio ou dependencia de domínio
├── app.py # Ponto de entrada da aplicação
├── docker-compose.yml # Configuração Docker
├── Dockerfile # Imagem Docker
└── requirements.txt # Dependências Python
```
- TODO: temos duas rotas (routes e controllers)? Unificar futuramente.
- TODO: sql/migrate_db parece utilizar outro banco.
## 🤝 Contribuição
1. Crie uma branch para sua feature
2. Commit suas mudanças
3. Push sua branch para o Gitea
4. Outro camarada verifica a branch
5. Abra um Pull Request para a branch solicitada
## 📄 Licença
Este projeto é privado para uso da OCI.
## 🔍 Troubleshooting
1. **Redis não conecta**
```bash
docker-compose logs redis
docker-compose restart redis
```
- Redis está indisponível localmente, mas app continua executando mesmo fora do Docker.
2. **Cache não funciona**
```bash
make cache-status
make cache-clear
```
3. **Aplicação não inicia**
```bash
docker-compose logs app
docker-compose down && docker-compose up -d
```
4. **Modificações no banco local não alteram o banco no Docker**
- Linux bind mount no grupo de usuario errado: ajuste `APP_UID`/`APP_GID` no `Dockerfile` para seu grupo de usuarios (padrão=1000).
- Docker com engine do Windows não consegue fazer bind mount, então alterações no banco local não refletem no banco do Docker, use as operações dentro do docker com make docker-* ou no windows instale o wsl2 e instale o Docker com apenas a engine "Docker no WSL".
## Estrutura de Permissões (RBAC) ## Estrutura de Permissões (RBAC)
O sistema utiliza um sistema de controle de acesso baseado em papéis (RBAC) com a seguinte hierarquia: O sistema utiliza um sistema de controle de acesso baseado em papéis (RBAC), onde a verificação de ações são feitas com permissões (permission), e as permissões são pré-definidas com base em papeis (role). Que possuem a seguinte hierarquia:
### Níveis de Papéis ### Níveis de Papéis
@@ -47,41 +295,7 @@ O sistema utiliza um sistema de controle de acesso baseado em papéis (RBAC) com
- Criar CRs - Criar CRs
- Configurar sistema - Configurar sistema
## Instalação ## Uso do RBAC
1. Clone o repositório
2. Crie um ambiente virtual:
```bash
python -m venv venv
source venv/bin/activate # Linux/Mac
# ou
venv\Scripts\activate # Windows
```
3. Instale as dependências:
```bash
pip install -r requirements.txt
```
4. Execute as migrações do banco de dados:
```bash
python sql/migrate_db.py
```
5. Configure as variáveis de ambiente no arquivo `.env`:
```
FLASK_APP=app.py
FLASK_ENV=development
SECRET_KEY=sua_chave_secreta
MAIL_SERVER=seu_servidor_smtp
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=seu_email
MAIL_PASSWORD=sua_senha
```
6. Execute o aplicativo:
```bash
flask run
```
## Uso
### Decoradores de Permissão ### Decoradores de Permissão
@@ -99,31 +313,32 @@ O sistema fornece três decoradores para controle de acesso:
- Verifica se o usuário tem um papel com nível mínimo - Verifica se o usuário tem um papel com nível mínimo
- Exemplo: `@require_minimum_role(Role.SECRETARIO_CR)` - Exemplo: `@require_minimum_role(Role.SECRETARIO_CR)`
### Verificando Permissões no Código ### Verificando Permissões e Papéis no Código
```python ```python
# Verificar se um usuário tem uma permissão # Verificar se um usuário tem uma permissão
if user.has_permission('create_cell_member'): if user.has_permission(Permission.CREATE_CELL_MEMBER):
# Faça algo # Faça algo
# Verificar se um usuário tem um papel # Verificar se um usuário tem um papel
if user.has_role('Secretário de Célula'): if user.has_role(Role.SECRETARIO_CELULA):
# Faça algo # Faça algo
# Obter o papel mais alto do usuário # Obter o papel mais alto do usuário
highest_role = user.get_highest_role() highest_role = user.get_highest_role()
if highest_role and highest_role.nivel >= Role.SECRETARIO_CR:
# Verificar se o usuário tem nível secretário de célula ou superior
if user.has_minimum_role(Role.SECRETARIO_CELULA):
# Faça algo # Faça algo
``` ```
## Estrutura do Banco de Dados ## Documentação Complementar
O sistema utiliza as seguintes tabelas para o RBAC: - Documentação complementar: `docs/README.md`
- RBAC: `docs/rbac.md`
- `roles`: Armazena os papéis disponíveis - Estratégia de permissões: `docs/permission_strategy.md`
- `permissions`: Armazena as permissões disponíveis - Redis e cache: `docs/redis_cache_setup.md`
- `role_permissions`: Mapeia papéis para permissões - Histórico de correções de permissões: `docs/permission_fixes_summary.md`
- `user_roles`: Mapeia usuários para papéis
## Segurança ## Segurança

54
app.py
View File

@@ -1,19 +1,24 @@
import os
import secrets
import sys
import logging
import time
from pathlib import Path
from flask import Flask from flask import Flask
from flask_bootstrap5 import Bootstrap from flask_bootstrap5 import Bootstrap
from flask_login import LoginManager from flask_login import LoginManager
from flask_wtf.csrf import CSRFProtect from flask_wtf.csrf import CSRFProtect
from flask_mail import Mail from flask_mail import Mail
from functions.database import get_db_connection, Usuario
from functions.rbac import init_rbac
from functions.template_helpers import permission_context_processor, init_template_filters, safe_render_helper
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
import os
import secrets
from dotenv import load_dotenv from dotenv import load_dotenv
import sys
import logging # Carregar .env antes de importar módulos
load_dotenv(Path(__file__).resolve().parent / ".env")
from functions.database import get_db_session, Usuario
from functions.rbac import Role
from functions.template_helpers import permission_context_processor, init_template_filters, safe_render_helper
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
import time
# Importar blueprints # Importar blueprints
from controllers.auth_controller import auth_bp from controllers.auth_controller import auth_bp
@@ -28,8 +33,6 @@ from routes.admin import admin_bp
# Import cache service # Import cache service
from services.cache_service import cache_service from services.cache_service import cache_service
load_dotenv()
def setup_logging(app): def setup_logging(app):
"""Configure logging for the application""" """Configure logging for the application"""
if not app.debug and not app.testing: if not app.debug and not app.testing:
@@ -99,12 +102,11 @@ def create_app():
@login_manager.user_loader @login_manager.user_loader
def load_user(user_id): def load_user(user_id):
"""Carrega o usuário pelo ID""" """Carrega o usuário pelo ID com roles e permissions (eager)."""
db = get_db_connection() db = get_db_session()
try: try:
# Carregar o usuário com suas roles
user = db.query(Usuario).options( user = db.query(Usuario).options(
joinedload(Usuario.roles) joinedload(Usuario.roles).joinedload(Role.permissions)
).get(user_id) ).get(user_id)
return user return user
finally: finally:
@@ -162,21 +164,6 @@ def create_app():
return app return app
def init_system():
"""Inicializa o sistema"""
print("Inicializando sistema...")
# Inicializar RBAC
print("Inicializando RBAC...")
init_rbac()
# Criar usuário admin se não existir
from create_admin import create_admin_user
print("Criando usuário admin...")
create_admin_user()
print("Sistema inicializado com sucesso!")
def main(): def main():
"""Função principal""" """Função principal"""
# Criar a aplicação # Criar a aplicação
@@ -187,10 +174,11 @@ def main():
app = main() app = main()
if __name__ == '__main__': if __name__ == '__main__':
# Verificar se é para inicializar o sistema if len(sys.argv) > 1:
if '--init' in sys.argv: print("app.py não aceita argumentos.")
init_system() print("Use 'python scripts/manage.py --help' para comandos administrativos.")
else: raise SystemExit(2)
app.run( app.run(
host='0.0.0.0', host='0.0.0.0',
port=5000, port=5000,

View File

@@ -1,13 +1,10 @@
from flask import Blueprint, request, render_template, redirect, url_for, flash, session, jsonify from flask import Blueprint, request, render_template, redirect, url_for, flash, session, jsonify
from flask_login import login_user, logout_user, current_user from flask_login import login_user, logout_user, current_user
from datetime import datetime from datetime import datetime
from functions.database import get_db_connection, Usuario from functions.database import Militante, get_db_session, Usuario
from functions.decorators import require_login from functions.decorators import require_login
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
import pyotp from services.otp_service import generate_qr_code_base64
import qrcode
import base64
from io import BytesIO
auth_bp = Blueprint('auth', __name__) auth_bp = Blueprint('auth', __name__)
@@ -30,7 +27,7 @@ def login():
flash("Email/usuário e senha são obrigatórios.", "danger") flash("Email/usuário e senha são obrigatórios.", "danger")
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))
db = get_db_connection() db = get_db_session()
try: try:
# Tenta encontrar o usuário por email ou username # Tenta encontrar o usuário por email ou username
user = db.query(Usuario).filter( user = db.query(Usuario).filter(
@@ -105,7 +102,7 @@ def api_login():
'error': 'Email/username e senha são obrigatórios' 'error': 'Email/username e senha são obrigatórios'
}), 400 }), 400
db = get_db_connection() db = get_db_session()
try: try:
# Buscar usuário # Buscar usuário
user = db.query(Usuario).filter( user = db.query(Usuario).filter(
@@ -182,7 +179,7 @@ def api_logout():
"""Endpoint de logout API""" """Endpoint de logout API"""
try: try:
if current_user.is_authenticated: if current_user.is_authenticated:
db = get_db_connection() db = get_db_session()
try: try:
user = current_user user = current_user
user.logout() user.logout()
@@ -226,7 +223,7 @@ def api_status():
@auth_bp.route("/logout") @auth_bp.route("/logout")
@require_login @require_login
def logout(): def logout():
db = get_db_connection() db = get_db_session()
try: try:
user = current_user user = current_user
if user: if user:
@@ -255,7 +252,7 @@ def alterar_senha():
flash("As senhas não coincidem.", "error") flash("As senhas não coincidem.", "error")
return redirect(url_for("auth.alterar_senha")) return redirect(url_for("auth.alterar_senha"))
db = get_db_connection() db = get_db_session()
try: try:
user = db.query(Usuario).get(current_user.id) user = db.query(Usuario).get(current_user.id)
if not user.check_password(senha_atual): if not user.check_password(senha_atual):
@@ -274,31 +271,14 @@ def alterar_senha():
@auth_bp.route("/qr/<token>") @auth_bp.route("/qr/<token>")
def get_qr_code(token): def get_qr_code(token):
"""Gera QR code para configuração OTP""" """Gera QR code para configuração OTP"""
db = get_db_connection() db = get_db_session()
try: try:
militante = db.query(Militante).filter_by(temp_token=token).first() militante = db.query(Militante).filter_by(temp_token=token).first()
if not militante or militante.temp_token_expiry < datetime.now(): if not militante or militante.temp_token_expiry < datetime.now():
flash('Token inválido ou expirado.', 'danger') flash('Token inválido ou expirado.', 'danger')
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
qr_code = generate_qr_code(militante) qr_code = generate_qr_code_base64(militante)
return render_template('mostrar_qr_code.html', qr_code=qr_code) return render_template('mostrar_qr_code.html', qr_code=qr_code)
finally: finally:
db.close() db.close()
def generate_qr_code(user):
"""Gera um QR code para o usuário"""
if not user.otp_secret:
user.otp_secret = pyotp.random_base32()
totp = pyotp.TOTP(user.otp_secret)
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(totp.provisioning_uri(user.email, issuer_name="Sistema de Controles"))
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buffer = BytesIO()
img.save(buffer, format="PNG")
qr_code = base64.b64encode(buffer.getvalue()).decode('utf-8')
return qr_code

View File

@@ -1,5 +1,5 @@
from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify
from functions.database import get_db_connection, CotaMensal, Militante from functions.database import get_db_session, CotaMensal, Militante
from functions.decorators import require_login from functions.decorators import require_login
from utils.date_utils import validar_data, converter_data from utils.date_utils import validar_data, converter_data
from datetime import datetime from datetime import datetime
@@ -12,6 +12,7 @@ cota_bp = Blueprint('cota', __name__)
def novo(): def novo():
"""Cria uma nova cota mensal""" """Cria uma nova cota mensal"""
if request.method == "POST": if request.method == "POST":
db = get_db_session()
try: try:
militante_id = request.form.get("militante_id") militante_id = request.form.get("militante_id")
valor_antigo = float(request.form.get("valor_antigo")) valor_antigo = float(request.form.get("valor_antigo"))
@@ -23,7 +24,6 @@ def novo():
flash('Data inválida ou futura', 'danger') flash('Data inválida ou futura', 'danger')
return redirect(url_for('cota.novo')) return redirect(url_for('cota.novo'))
db = get_db_connection()
cota = CotaMensal( cota = CotaMensal(
militante_id=militante_id, militante_id=militante_id,
valor_antigo=valor_antigo, valor_antigo=valor_antigo,
@@ -44,7 +44,7 @@ def novo():
db.close() db.close()
# GET - Renderizar formulário # GET - Renderizar formulário
db = get_db_connection() db = get_db_session()
try: try:
militantes = db.query(Militante).order_by(Militante.nome).all() militantes = db.query(Militante).order_by(Militante.nome).all()
return render_template("nova_cota.html", militantes=militantes) return render_template("nova_cota.html", militantes=militantes)
@@ -55,7 +55,7 @@ def novo():
@require_login @require_login
def listar(): def listar():
"""Lista todas as cotas mensais com controle de permissões no nível de dados""" """Lista todas as cotas mensais com controle de permissões no nível de dados"""
db = get_db_connection() db = get_db_session()
try: try:
# SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões # SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões
cotas = [] cotas = []
@@ -82,7 +82,7 @@ def listar():
@require_login @require_login
def editar(id): def editar(id):
"""Edita uma cota mensal""" """Edita uma cota mensal"""
db = get_db_connection() db = get_db_session()
try: try:
cota = db.query(CotaMensal).get(id) cota = db.query(CotaMensal).get(id)
if not cota: if not cota:
@@ -114,7 +114,7 @@ def editar(id):
@require_login @require_login
def excluir(id): def excluir(id):
"""Exclui uma cota mensal""" """Exclui uma cota mensal"""
db = get_db_connection() db = get_db_session()
try: try:
cota = db.query(CotaMensal).get(id) cota = db.query(CotaMensal).get(id)
if not cota: if not cota:

View File

@@ -1,5 +1,5 @@
from flask import Blueprint, render_template, flash, redirect, url_for, jsonify from flask import Blueprint, render_template, flash, redirect, url_for, jsonify
from functions.database import get_db_connection, Militante, Pagamento, CotaMensal, MaterialVendido, AssinaturaAnual, TipoPagamento from functions.database import get_db_session, Militante, Pagamento, CotaMensal, MaterialVendido, AssinaturaAnual, TipoPagamento
from functions.decorators import require_login from functions.decorators import require_login
from datetime import datetime from datetime import datetime
from sqlalchemy import func from sqlalchemy import func
@@ -28,7 +28,7 @@ def dashboard():
stats = DashboardService.get_dashboard_stats() stats = DashboardService.get_dashboard_stats()
# Get tipos de pagamento for the modal # Get tipos de pagamento for the modal
db = get_db_connection() db = get_db_session()
try: try:
tipos_pagamento = db.query(TipoPagamento).all() tipos_pagamento = db.query(TipoPagamento).all()
finally: finally:

View File

@@ -1,5 +1,5 @@
from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify
from functions.database import get_db_connection, MaterialVendido, Militante, TipoMaterial from functions.database import get_db_session, MaterialVendido, Militante, TipoMaterial
from functions.decorators import require_login from functions.decorators import require_login
from utils.date_utils import validar_data, converter_data from utils.date_utils import validar_data, converter_data
from datetime import datetime from datetime import datetime
@@ -11,7 +11,7 @@ material_bp = Blueprint('material', __name__)
@require_login @require_login
def listar(): def listar():
"""Lista todos os materiais com controle de permissões no nível de dados""" """Lista todos os materiais com controle de permissões no nível de dados"""
db = get_db_connection() db = get_db_session()
try: try:
# SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões # SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões
materiais = [] materiais = []
@@ -46,6 +46,7 @@ def listar():
def novo(): def novo():
"""Cria um novo material vendido""" """Cria um novo material vendido"""
if request.method == "POST": if request.method == "POST":
db = get_db_session()
try: try:
militante_id = request.form.get("militante_id") militante_id = request.form.get("militante_id")
tipo_material_id = request.form.get("tipo_material_id") tipo_material_id = request.form.get("tipo_material_id")
@@ -57,7 +58,6 @@ def novo():
flash('Data de venda inválida ou futura', 'danger') flash('Data de venda inválida ou futura', 'danger')
return redirect(url_for('material.novo')) return redirect(url_for('material.novo'))
db = get_db_connection()
material = MaterialVendido( material = MaterialVendido(
militante_id=militante_id, militante_id=militante_id,
tipo_material_id=tipo_material_id, tipo_material_id=tipo_material_id,
@@ -77,7 +77,7 @@ def novo():
db.close() db.close()
# GET - Renderizar formulário # GET - Renderizar formulário
db = get_db_connection() db = get_db_session()
try: try:
militantes = db.query(Militante).order_by(Militante.nome).all() militantes = db.query(Militante).order_by(Militante.nome).all()
tipos_material = db.query(TipoMaterial).order_by(TipoMaterial.descricao).all() tipos_material = db.query(TipoMaterial).order_by(TipoMaterial.descricao).all()
@@ -89,7 +89,7 @@ def novo():
@require_login @require_login
def editar(id): def editar(id):
"""Edita um material vendido""" """Edita um material vendido"""
db = get_db_connection() db = get_db_session()
try: try:
material = db.query(MaterialVendido).get(id) material = db.query(MaterialVendido).get(id)
if not material: if not material:
@@ -122,7 +122,7 @@ def editar(id):
@require_login @require_login
def excluir(id): def excluir(id):
"""Exclui um material vendido""" """Exclui um material vendido"""
db = get_db_connection() db = get_db_session()
try: try:
material = db.query(MaterialVendido).get(id) material = db.query(MaterialVendido).get(id)
if not material: if not material:
@@ -146,7 +146,7 @@ def excluir(id):
@require_login @require_login
def listar_tipos(): def listar_tipos():
"""Lista todos os tipos de materiais com controle de permissões no nível de dados""" """Lista todos os tipos de materiais com controle de permissões no nível de dados"""
db = get_db_connection() db = get_db_session()
try: try:
# SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões # SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões
tipos_materiais = [] tipos_materiais = []
@@ -174,10 +174,10 @@ def listar_tipos():
def novo_tipo(): def novo_tipo():
"""Cria um novo tipo de material""" """Cria um novo tipo de material"""
if request.method == "POST": if request.method == "POST":
db = get_db_session()
try: try:
descricao = request.form.get("descricao") descricao = request.form.get("descricao")
db = get_db_connection()
tipo = TipoMaterial(descricao=descricao) tipo = TipoMaterial(descricao=descricao)
db.add(tipo) db.add(tipo)
db.commit() db.commit()
@@ -196,7 +196,7 @@ def novo_tipo():
@require_login @require_login
def editar_tipo(id): def editar_tipo(id):
"""Edita um tipo de material""" """Edita um tipo de material"""
db = get_db_connection() db = get_db_session()
try: try:
tipo = db.query(TipoMaterial).get(id) tipo = db.query(TipoMaterial).get(id)
if not tipo: if not tipo:
@@ -222,7 +222,7 @@ def editar_tipo(id):
@require_login @require_login
def excluir_tipo(id): def excluir_tipo(id):
"""Exclui um tipo de material""" """Exclui um tipo de material"""
db = get_db_connection() db = get_db_session()
try: try:
tipo = db.query(TipoMaterial).get(id) tipo = db.query(TipoMaterial).get(id)
if not tipo: if not tipo:

View File

@@ -1,5 +1,5 @@
from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify
from functions.database import get_db_connection, Militante, EmailMilitante, Endereco, Celula, Setor, ComiteRegional from functions.database import get_db_session, safe_rollback, Militante, EmailMilitante, Endereco, Celula, Setor, ComiteRegional
from functions.decorators import require_login from functions.decorators import require_login
from functions.validations import validar_cpf from functions.validations import validar_cpf
from functions.rbac import Permission from functions.rbac import Permission
@@ -14,6 +14,7 @@ militante_bp = Blueprint('militante', __name__)
@require_login @require_login
def criar(): def criar():
"""Cria um novo militante""" """Cria um novo militante"""
db = get_db_session()
try: try:
data = request.get_json() data = request.get_json()
@@ -30,8 +31,6 @@ def criar():
'message': 'CPF inválido' 'message': 'CPF inválido'
}), 400 }), 400
db = get_db_connection()
# Verificar se CPF já existe # Verificar se CPF já existe
if db.query(Militante).filter_by(cpf=data['cpf']).first(): if db.query(Militante).filter_by(cpf=data['cpf']).first():
return jsonify({ return jsonify({
@@ -104,7 +103,7 @@ def criar():
@require_login @require_login
def listar(): def listar():
"""Lista todos os militantes com controle de permissões no nível de dados""" """Lista todos os militantes com controle de permissões no nível de dados"""
db = get_db_connection() db = get_db_session()
try: try:
# SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões # SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões
militantes = [] militantes = []
@@ -182,7 +181,7 @@ def listar():
@require_login @require_login
def excluir(id): def excluir(id):
"""Exclui um militante""" """Exclui um militante"""
db = get_db_connection() db = get_db_session()
try: try:
militante = db.query(Militante).get(id) militante = db.query(Militante).get(id)
if not militante: if not militante:
@@ -211,10 +210,9 @@ def excluir(id):
@require_login @require_login
def editar(militante_id): def editar(militante_id):
"""Edita um militante existente""" """Edita um militante existente"""
db = get_db_session()
try: try:
data = request.get_json() data = request.get_json()
db = get_db_connection()
militante = db.query(Militante).get(militante_id) militante = db.query(Militante).get(militante_id)
if not militante: if not militante:
@@ -283,7 +281,7 @@ def editar(militante_id):
@require_login @require_login
def buscar_dados(militante_id): def buscar_dados(militante_id):
"""Busca os dados de um militante específico""" """Busca os dados de um militante específico"""
db = get_db_connection() db = get_db_session()
try: try:
militante = db.query(Militante).options( militante = db.query(Militante).options(
joinedload(Militante.emails), joinedload(Militante.emails),
@@ -359,7 +357,7 @@ def buscar_dados(militante_id):
@require_login @require_login
def get_setores(cr_id): def get_setores(cr_id):
"""Retorna setores de um CR específico""" """Retorna setores de um CR específico"""
db = get_db_connection() db = get_db_session()
try: try:
setores = db.query(Setor).filter_by(cr_id=cr_id).all() setores = db.query(Setor).filter_by(cr_id=cr_id).all()
return jsonify([{'id': s.id, 'nome': s.nome} for s in setores]) return jsonify([{'id': s.id, 'nome': s.nome} for s in setores])

View File

@@ -1,5 +1,5 @@
from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify
from functions.database import get_db_connection, Pagamento, Militante, TipoPagamento from functions.database import get_db_session, Pagamento, Militante, TipoPagamento
from functions.decorators import require_login from functions.decorators import require_login
from utils.date_utils import validar_data, converter_data from utils.date_utils import validar_data, converter_data
from datetime import datetime from datetime import datetime
@@ -12,6 +12,7 @@ pagamento_bp = Blueprint('pagamento', __name__)
def novo(): def novo():
"""Cria um novo pagamento""" """Cria um novo pagamento"""
if request.method == "POST": if request.method == "POST":
db = get_db_session()
try: try:
militante_id = request.form.get("militante_id") militante_id = request.form.get("militante_id")
tipo_pagamento_id = request.form.get("tipo_pagamento_id") tipo_pagamento_id = request.form.get("tipo_pagamento_id")
@@ -22,7 +23,6 @@ def novo():
flash('Data de pagamento inválida ou futura', 'danger') flash('Data de pagamento inválida ou futura', 'danger')
return redirect(url_for('pagamento.novo')) return redirect(url_for('pagamento.novo'))
db = get_db_connection()
pagamento = Pagamento( pagamento = Pagamento(
militante_id=militante_id, militante_id=militante_id,
tipo_pagamento_id=tipo_pagamento_id, tipo_pagamento_id=tipo_pagamento_id,
@@ -41,7 +41,7 @@ def novo():
db.close() db.close()
# GET - Renderizar formulário # GET - Renderizar formulário
db = get_db_connection() db = get_db_session()
try: try:
militantes = db.query(Militante).order_by(Militante.nome).all() militantes = db.query(Militante).order_by(Militante.nome).all()
tipos_pagamento = db.query(TipoPagamento).order_by(TipoPagamento.descricao).all() tipos_pagamento = db.query(TipoPagamento).order_by(TipoPagamento.descricao).all()
@@ -53,7 +53,7 @@ def novo():
@require_login @require_login
def listar(): def listar():
"""Lista todos os pagamentos com controle de permissões no nível de dados""" """Lista todos os pagamentos com controle de permissões no nível de dados"""
db = get_db_connection() db = get_db_session()
try: try:
# SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões # SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões
pagamentos = [] pagamentos = []
@@ -88,13 +88,13 @@ def listar():
def adicionar(): def adicionar():
"""Adiciona um novo pagamento""" """Adiciona um novo pagamento"""
if request.method == "POST": if request.method == "POST":
db = get_db_session()
try: try:
militante_id = request.form.get("militante_id") militante_id = request.form.get("militante_id")
tipo_pagamento = request.form.get("tipo_pagamento") tipo_pagamento = request.form.get("tipo_pagamento")
valor = float(request.form.get("valor")) valor = float(request.form.get("valor"))
data_pagamento = converter_data(request.form.get("data_pagamento")) data_pagamento = converter_data(request.form.get("data_pagamento"))
db = get_db_connection()
pagamento = Pagamento( pagamento = Pagamento(
militante_id=militante_id, militante_id=militante_id,
tipo_pagamento=tipo_pagamento, tipo_pagamento=tipo_pagamento,
@@ -116,7 +116,7 @@ def adicionar():
@require_login @require_login
def list_pagamentos_celula(celula_id): def list_pagamentos_celula(celula_id):
"""Lista pagamentos de uma célula específica""" """Lista pagamentos de uma célula específica"""
db = get_db_connection() db = get_db_session()
try: try:
pagamentos = db.query(Pagamento).filter_by(celula_id=celula_id).all() pagamentos = db.query(Pagamento).filter_by(celula_id=celula_id).all()
return render_template('pagamentos/list.html', pagamentos=pagamentos) return render_template('pagamentos/list.html', pagamentos=pagamentos)
@@ -127,7 +127,7 @@ def list_pagamentos_celula(celula_id):
@require_login @require_login
def list_pagamentos_setor(setor_id): def list_pagamentos_setor(setor_id):
"""Lista pagamentos de um setor específico""" """Lista pagamentos de um setor específico"""
db = get_db_connection() db = get_db_session()
try: try:
pagamentos = db.query(Pagamento).join(Usuario).filter(Usuario.setor_id == setor_id).all() pagamentos = db.query(Pagamento).join(Usuario).filter(Usuario.setor_id == setor_id).all()
return render_template('pagamentos/list.html', pagamentos=pagamentos) return render_template('pagamentos/list.html', pagamentos=pagamentos)
@@ -138,7 +138,7 @@ def list_pagamentos_setor(setor_id):
@require_login @require_login
def list_pagamentos_cr(cr_id): def list_pagamentos_cr(cr_id):
"""Lista pagamentos de um CR específico""" """Lista pagamentos de um CR específico"""
db = get_db_connection() db = get_db_session()
try: try:
pagamentos = db.query(Pagamento).join(Usuario).filter(Usuario.cr_id == cr_id).all() pagamentos = db.query(Pagamento).join(Usuario).filter(Usuario.cr_id == cr_id).all()
return render_template('pagamentos/list.html', pagamentos=pagamentos) return render_template('pagamentos/list.html', pagamentos=pagamentos)
@@ -150,7 +150,7 @@ def list_pagamentos_cr(cr_id):
def novo_pagamento_celula(celula_id): def novo_pagamento_celula(celula_id):
"""Cria novo pagamento para uma célula""" """Cria novo pagamento para uma célula"""
if request.method == 'POST': if request.method == 'POST':
db = get_db_connection() db = get_db_session()
try: try:
pagamento = Pagamento( pagamento = Pagamento(
valor=request.form['valor'], valor=request.form['valor'],
@@ -171,7 +171,7 @@ def novo_pagamento_celula(celula_id):
def novo_pagamento_setor(setor_id): def novo_pagamento_setor(setor_id):
"""Cria novo pagamento para um setor""" """Cria novo pagamento para um setor"""
if request.method == 'POST': if request.method == 'POST':
db = get_db_connection() db = get_db_session()
try: try:
pagamento = Pagamento( pagamento = Pagamento(
valor=request.form['valor'], valor=request.form['valor'],
@@ -192,7 +192,7 @@ def novo_pagamento_setor(setor_id):
def novo_pagamento_cr(cr_id): def novo_pagamento_cr(cr_id):
"""Cria novo pagamento para um CR""" """Cria novo pagamento para um CR"""
if request.method == 'POST': if request.method == 'POST':
db = get_db_connection() db = get_db_session()
try: try:
pagamento = Pagamento( pagamento = Pagamento(
valor=request.form['valor'], valor=request.form['valor'],

View File

@@ -1,5 +1,5 @@
from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify
from functions.database import get_db_connection, Usuario, Role, Setor from functions.database import get_db_session, Usuario, Role, Setor
from functions.decorators import require_login from functions.decorators import require_login
from flask_login import current_user from flask_login import current_user
import pyotp import pyotp
@@ -18,7 +18,7 @@ def novo():
setor_id = request.form.get("setor_id") setor_id = request.form.get("setor_id")
# Verificar se usuário já existe # Verificar se usuário já existe
db = get_db_connection() db = get_db_session()
try: try:
if db.query(Usuario).filter_by(username=username).first(): if db.query(Usuario).filter_by(username=username).first():
flash('Nome de usuário já existe.', 'danger') flash('Nome de usuário já existe.', 'danger')
@@ -45,7 +45,7 @@ def novo():
finally: finally:
db.close() db.close()
db = get_db_connection() db = get_db_session()
try: try:
roles = db.query(Role).order_by(Role.nome).all() roles = db.query(Role).order_by(Role.nome).all()
setores = db.query(Setor).order_by(Setor.nome).all() setores = db.query(Setor).order_by(Setor.nome).all()
@@ -63,7 +63,7 @@ def toggle_status(user_id):
'error': 'Você não tem permissão para alterar o status de usuários.' 'error': 'Você não tem permissão para alterar o status de usuários.'
}), 403 }), 403
db = get_db_connection() db = get_db_session()
try: try:
usuario = db.query(Usuario).get(user_id) usuario = db.query(Usuario).get(user_id)
if not usuario: if not usuario:
@@ -105,7 +105,7 @@ def alterar_nivel(user_id):
'error': 'Novo nível não especificado.' 'error': 'Novo nível não especificado.'
}), 400 }), 400
db = get_db_connection() db = get_db_session()
try: try:
usuario = db.query(Usuario).get(user_id) usuario = db.query(Usuario).get(user_id)
if not usuario: if not usuario:
@@ -150,7 +150,7 @@ def toggle_quadro_orientador(user_id):
'error': 'Você não tem permissão para alterar responsabilidades de usuários.' 'error': 'Você não tem permissão para alterar responsabilidades de usuários.'
}), 403 }), 403
db = get_db_connection() db = get_db_session()
try: try:
usuario = db.query(Usuario).get(user_id) usuario = db.query(Usuario).get(user_id)
if not usuario: if not usuario:

View File

@@ -1,171 +0,0 @@
from functions.database import init_database, Usuario, Role, get_db_connection
import qrcode
import os
from pathlib import Path
import pyotp
def generate_qr_code(user):
"""
Gera o QR code para um usuário específico
Args:
user: Instância do modelo Usuario
Returns:
tuple: (caminho do arquivo, URI do OTP)
"""
# Tentar diferentes caminhos para salvar o QR code
qr_paths = [
Path('/tmp/admin_qr.png'), # Diretório temporário do sistema
Path('admin_qr.png'), # Diretório atual
Path('/app/admin_qr.png') # Diretório da aplicação
]
# Gerar e salvar QR Code
qr = qrcode.QRCode(version=1, box_size=10, border=5)
# Gerar URI do OTP
totp = pyotp.TOTP(user.otp_secret)
otp_uri = totp.provisioning_uri(
name=user.username,
issuer_name="Sistema de Controles"
)
qr.add_data(otp_uri)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
# Tentar salvar em diferentes locais
qr_saved = False
saved_path = None
for qr_path in qr_paths:
try:
# Tentar salvar o arquivo
img.save(str(qr_path))
print(f"QR Code salvo em: {qr_path}")
qr_saved = True
saved_path = qr_path
break
except Exception as e:
print(f"Não foi possível salvar o QR code em {qr_path}: {e}")
continue
if not qr_saved:
print("AVISO: Não foi possível salvar o QR code em nenhum local")
print("O QR code pode ser gerado manualmente usando o URI OTP")
saved_path = None
return saved_path, otp_uri
def create_admin_user():
"""Cria ou atualiza o usuário admin"""
try:
# Inicializar banco de dados
init_database()
# Criar sessão
db = get_db_connection()
try:
# Verificar se já existe um usuário admin
admin = db.query(Usuario).filter_by(username="admin").first()
if admin:
print("\n=== Usuário Admin Encontrado ===")
if not admin.otp_secret:
print("Gerando novo segredo OTP...")
admin.generate_otp_secret()
db.commit()
else:
print("\n=== Criando Novo Usuário Admin ===")
# Criar novo usuário admin
admin = Usuario(
username="admin",
email="admin@example.com",
is_admin=True
)
admin.set_password("admin123")
admin.generate_otp_secret()
# Buscar ou criar role de admin
admin_role = db.query(Role).filter_by(nome="admin").first()
if not admin_role:
admin_role = Role(nome="admin", nivel=0) # Nível 0 é o mais alto
db.add(admin_role)
# Adicionar role ao usuário
admin.roles.append(admin_role)
# Adicionar e fazer commit
db.add(admin)
db.commit()
# Gerar QR code
qr_path, otp_uri = generate_qr_code(admin)
if qr_path:
print("\n=== QR Code Gerado ===")
print(f"QR Code salvo em: {qr_path}")
print(f"URI do OTP: {otp_uri}")
else:
print("\n=== QR Code Não Pode Ser Salvo ===")
print("Use o URI OTP para configuração manual:")
print(f"URI do OTP: {otp_uri}")
# Mostrar informações
print("\n=== Informações do Admin ===")
print(f"Username: {admin.username}")
print(f"Email: {admin.email}")
print(f"Senha: admin123")
print(f"Segredo OTP: {admin.otp_secret}")
# Gerar código atual para verificação
totp = pyotp.TOTP(admin.otp_secret)
current_code = totp.now()
print("\n=== Verificação do OTP ===")
print(f"Código OTP atual: {current_code}")
print(f"Verificação do código: {totp.verify(current_code)}")
print("\n=== Instruções para Configuração ===")
print("1. Instale um aplicativo autenticador no seu celular")
print(" (Google Authenticator, Microsoft Authenticator, etc)")
print("2. Abra o aplicativo")
print("3. Selecione a opção para adicionar uma nova conta")
if qr_path:
print("4. Escaneie o QR Code salvo em:", qr_path)
print("\nOU configure manualmente:")
print(f"- Nome da conta: {admin.username}")
print(f"- Segredo: {admin.otp_secret}")
print("- Tipo: Baseado em tempo (TOTP)")
print("- Algoritmo: SHA1")
print("- Dígitos: 6")
print("- Intervalo: 30 segundos")
# Verificação final
print("\n=== Teste de Verificação ===")
test_code = totp.now()
print(f"Código de teste: {test_code}")
is_valid = admin.verify_otp(test_code)
print(f"Verificação do código: {'Sucesso' if is_valid else 'Falha'}")
if not is_valid:
print("\nALERTA: Verificação do OTP falhou!")
print("Por favor, verifique se o segredo OTP está correto.")
# Fazer commit final para garantir que tudo foi salvo
db.commit()
except Exception as e:
db.rollback()
raise e
finally:
db.close()
except Exception as e:
print(f"\nErro durante a execução: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
create_admin_user()

View File

@@ -0,0 +1,62 @@
services:
# Redis Cache Service
redis:
image: redis:7-alpine
container_name: controles_redis
volumes:
- redis_data:/data
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
restart: unless-stopped
healthcheck:
test: [ "CMD", "redis-cli", "ping" ]
interval: 30s
timeout: 10s
retries: 3
networks:
- controles_network
# Flask Application
app:
build: .
container_name: controles_app
ports:
- "5000:5000"
environment:
- FLASK_APP=app.py
- FLASK_ENV=production
- REDIS_URL=redis://redis:6379/0
- DATABASE_URL=sqlite:////data/database.db
# DEV apenas para facilitar testes, não deve ser usado em produção
- ADMIN_OTP_SECRET=JBSWY3DPEHPK3PXP
# Produção definir em .env
#- ADMIN_OTP_SECRET=${ADMIN_OTP_SECRET}
volumes:
- app_data:/data
- app_logs:/app/logs
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
depends_on:
redis:
condition: service_healthy
restart: unless-stopped
networks:
- controles_network
volumes:
redis_data:
driver: local
app_data:
driver: local
app_logs:
driver: local
networks:
controles_network:
driver: bridge

View File

@@ -1,12 +1,8 @@
version: '3.8'
services: services:
# Redis Cache Service # Redis Cache Service
redis: redis:
image: redis:7-alpine image: redis:7-alpine
container_name: controles_redis container_name: controles_redis
ports:
- "6379:6379"
volumes: volumes:
- redis_data:/data - redis_data:/data
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
@@ -21,7 +17,11 @@ services:
# Flask Application # Flask Application
app: app:
build: . build:
context: .
args:
APP_UID: ${APP_UID:-1000}
APP_GID: ${APP_GID:-1000}
container_name: controles_app container_name: controles_app
ports: ports:
- "5000:5000" - "5000:5000"
@@ -29,11 +29,23 @@ services:
- FLASK_APP=app.py - FLASK_APP=app.py
- FLASK_ENV=production - FLASK_ENV=production
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
- DATABASE_URL=sqlite:///app/database.db - DATABASE_URL=sqlite:////data/database.db
# DEV apenas para facilitar testes, não deve ser usado em produção
- ADMIN_OTP_SECRET=JBSWY3DPEHPK3PXP - ADMIN_OTP_SECRET=JBSWY3DPEHPK3PXP
# Produção definir em .env
#- ADMIN_OTP_SECRET=${ADMIN_OTP_SECRET}
volumes: volumes:
- ./database.db:/app/database.db - ./data:/data
- ./admin_qr.png:/app/admin_qr.png - ./logs:/app/logs
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
depends_on: depends_on:
redis: redis:
condition: service_healthy condition: service_healthy

View File

@@ -2,26 +2,41 @@
Sistema de gerenciamento para a Organização Comunista Internacionalista (OCI) com controle de militantes, cotas, pagamentos e materiais. Sistema de gerenciamento para a Organização Comunista Internacionalista (OCI) com controle de militantes, cotas, pagamentos e materiais.
## 🚀 Status Atual ## Trilha Recomendada de Leitura
**Sistema com Arquitetura de Permissões Corrigida** 1. `docs/architecture_summary.md`
- Aplicação Flask rodando com Docker 2. `docs/rbac.md`
- Redis cache integrado e funcionando 3. `docs/permission_strategy.md`
- Banco de dados SQLite inicializado 4. `docs/redis_cache_setup.md`
- Usuário admin configurado com OTP 5. `docs/permission_fixes_summary.md`
- 30 militantes de teste criados
- Estrutura organizacional completa
- **Sistema de permissões implementado no nível de dados**
- **Menus sempre visíveis, controle transparente**
## 🎯 Arquitetura de Permissões ## Índice por Tema
O sistema implementa uma estratégia de controle de permissões no **nível de dados**, garantindo que: ### Arquitetura
- **Menus permanecem sempre visíveis** - Não há restrições na interface - `docs/architecture_summary.md`: visão geral do estado da arquitetura.
- **Dados são filtrados por hierarquia** - Admin → CC → CR → Setor → Célula - `docs/mvc_refactoring.md`: detalhes da refatoração MVC.
- **Templates nunca quebram** - Sempre renderizam, mesmo com dados vazios
- **Tesoureiros têm poder adequado** - Podem fazer tudo que secretários fazem ### Permissões e Segurança de Acesso
- `docs/rbac.md`: níveis de papel e herança de permissões.
- `docs/permission_strategy.md`: estratégia de filtragem de dados e uso em templates.
- `docs/permission_fixes_summary.md`: resumo das correções aplicadas em permissões.
### Infra e Performance
- `docs/redis_cache_setup.md`: configuração e uso de cache Redis.
### Histórico Técnico
- `docs/alteracoes_db_connection.md`: alterações no gerenciamento de conexão/sessão de banco.
## Como Manter Esta Pasta Organizada
- Preferir um arquivo por assunto (evitar documentos muito amplos).
- Começar cada documento com contexto, problema e decisão.
- Registrar trade-offs e impactos de manutenção.
- Atualizar este índice sempre que um novo documento for criado.
### Diagrama da Arquitetura ### Diagrama da Arquitetura
@@ -46,87 +61,6 @@ graph TD
J --> K[Always Renders Successfully] J --> K[Always Renders Successfully]
``` ```
## 🏗️ Arquitetura
O sistema foi refatorado seguindo o padrão MVC (Model-View-Controller):
```
controles/
├── app.py # Ponto de entrada da aplicação
├── controllers/ # Controladores (lógica de rotas)
├── models/ # Modelos (operações de banco)
├── services/ # Serviços (lógica de negócio)
├── templates/ # Views (templates HTML)
├── static/ # Assets estáticos
└── functions/ # Funções utilitárias
```
## 🐳 Docker Setup
### Pré-requisitos
- Docker e Docker Compose instalados
- Porta 5000 disponível para a aplicação
- Porta 6379 disponível para Redis
### Inicialização Rápida
```bash
# Clonar o repositório
git clone <repository-url>
cd controles
# Iniciar o ambiente completo
make dev-up
# Verificar status
docker-compose ps
# Ver logs
make docker-logs
```
### Comandos Úteis
```bash
# Iniciar serviços
make dev-up
# Parar serviços
make dev-down
# Ver logs
make docker-logs
# Status do cache Redis
make cache-status
# Limpar cache
make cache-clear
# Reconstruir containers
make docker-build
```
## 🔐 Acesso ao Sistema
### Credenciais do Admin
- **URL**: http://localhost:5000
- **Usuário**: admin
- **Senha**: admin123
- **OTP Secret**: JBSWY3DPEHPK3PXP
### Configuração OTP
1. Instale um aplicativo autenticador (Google Authenticator, Microsoft Authenticator)
2. Configure manualmente:
- Nome: admin
- Segredo: JBSWY3DPEHPK3PXP
- Tipo: TOTP
- Algoritmo: SHA1
- Dígitos: 6
- Intervalo: 30 segundos
**OU** use o QR Code gerado em `/tmp/admin_qr.png` dentro do container.
## 📊 Funcionalidades ## 📊 Funcionalidades
### Gestão de Militantes ### Gestão de Militantes
@@ -152,80 +86,6 @@ make docker-build
- Relatórios de vendas - Relatórios de vendas
- Relatórios de pagamentos - Relatórios de pagamentos
## 🗄️ Banco de Dados
### Estrutura
- **SQLite** com SQLAlchemy ORM
- **Redis** para cache de performance
- Migrações automáticas
- Dados de teste incluídos
### Inicialização
O banco é inicializado automaticamente no primeiro startup com:
- 30 militantes de teste
- Estrutura organizacional completa
- Tipos de pagamento e materiais
- Usuário admin configurado
## 🔧 Tecnologias
- **Backend**: Flask 2.3.3
- **Frontend**: Bootstrap 5, HTML5, CSS3, JavaScript
- **Database**: SQLite + SQLAlchemy 2.0.21
- **Cache**: Redis 7.4.4
- **Authentication**: Flask-Login + OTP (pyotp)
- **Container**: Docker + Docker Compose
- **Server**: Gunicorn
## 📁 Estrutura de Arquivos
```
controles/
├── app.py # Aplicação principal
├── controllers/ # Controladores MVC
│ ├── auth_controller.py # Autenticação
│ ├── home_controller.py # Dashboard
│ ├── militante_controller.py # Militantes
│ ├── pagamento_controller.py # Pagamentos
│ ├── cota_controller.py # Cotas
│ └── usuario_controller.py # Usuários
├── models/ # Modelos de dados
├── services/ # Serviços de negócio
├── templates/ # Templates HTML
├── static/ # Assets estáticos
├── functions/ # Funções utilitárias
├── docs/ # Documentação
├── docker-compose.yml # Configuração Docker
├── Dockerfile # Imagem Docker
└── requirements.txt # Dependências Python
```
## 🚨 Problemas Resolvidos
### ✅ QR Code Admin
- **Problema**: Erro de permissão ao salvar QR code
- **Solução**: Múltiplos caminhos de fallback, salvamento em `/tmp/`
### ✅ Conexão Redis
- **Problema**: Falhas de conexão durante startup
- **Solução**: Retry logic com backoff exponencial
### ✅ Método OTP
- **Problema**: Método `generate_otp_secret` ausente
- **Solução**: Implementado na classe Usuario
### ✅ Rede Docker
- **Problema**: Serviços não se comunicavam
- **Solução**: Configuração explícita de redes
### ✅ Segredo OTP Inválido
- **Problema**: Segredo OTP não estava em formato base32 válido
- **Solução**: Alterado para `JBSWY3DPEHPK3PXP` (formato base32 válido)
### ✅ Verificação de Arquivo QR Code
- **Problema**: `PermissionError` ao verificar existência do arquivo
- **Solução**: Removida verificação de existência, implementado sistema de fallback
## 📈 Performance ## 📈 Performance
### Cache Redis ### Cache Redis
@@ -235,6 +95,7 @@ controles/
- API responses: Variável - API responses: Variável
### Monitoramento ### Monitoramento
```bash ```bash
# Status do cache # Status do cache
make cache-status make cache-status
@@ -246,68 +107,6 @@ make docker-logs
docker-compose logs redis docker-compose logs redis
``` ```
## 🔍 Troubleshooting
### Problemas Comuns
1. **Redis não conecta**
```bash
docker-compose logs redis
docker-compose restart redis
```
2. **Aplicação não inicia**
```bash
docker-compose logs app
docker-compose down && docker-compose up -d
```
3. **Cache não funciona**
```bash
make cache-status
make cache-clear
```
4. **Erro de OTP**
```bash
# Verificar se o segredo está correto
echo "JBSWY3DPEHPK3PXP" | base32 -d
```
5. **Erro de permissão QR Code**
```bash
# O QR code agora salva em /tmp/admin_qr.png
# Se não conseguir salvar, use configuração manual
```
### Logs
- Aplicação: `logs/controles.log`
- Cache: `logs/cache.log`
- Docker: `docker-compose logs`
## 📚 Documentação
- [Arquitetura MVC](docs/mvc_refactoring.md)
- [Sistema RBAC](docs/rbac.md)
- [Cache Redis](docs/redis_cache_setup.md)
- [Resumo da Arquitetura](docs/architecture_summary.md)
## 🤝 Contribuição
1. Fork o projeto
2. Crie uma branch para sua feature
3. Commit suas mudanças
4. Push para a branch
5. Abra um Pull Request
## 📄 Licença
Este projeto é privado para uso da OCI.
## 📞 Suporte
Para suporte técnico, entre em contato com a equipe de desenvolvimento.
## 📋 Recommended Next Steps ## 📋 Recommended Next Steps
### High Priority ### High Priority

View File

@@ -1,16 +1,17 @@
import os
from sqlalchemy import create_engine, text from sqlalchemy import create_engine, text
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from pathlib import Path from pathlib import Path
# Configurar caminho do banco de dados # Configurar caminho do banco de dados
db_dir = Path.home() / '.local' / 'share' / 'controles' db_path = Path(__file__).resolve().parents[1] / 'data' / 'database.db'
db_dir.mkdir(parents=True, exist_ok=True) db_path.parent.mkdir(parents=True, exist_ok=True)
db_path = db_dir / 'database.db' db_fallback = f'sqlite:///{db_path}'
# Configurar SQLite com opções para melhor concorrência # Configurar SQLite com opções para melhor concorrência
engine = create_engine( engine = create_engine(
f'sqlite:///{db_path}', os.environ.get('DATABASE_URL', db_fallback),
connect_args={ connect_args={
'timeout': 30, # Tempo de espera em segundos 'timeout': 30, # Tempo de espera em segundos
'check_same_thread': False # Permite acesso de múltiplas threads 'check_same_thread': False # Permite acesso de múltiplas threads
@@ -22,15 +23,15 @@ engine = create_engine(
Session = sessionmaker(bind=engine) Session = sessionmaker(bind=engine)
Base = declarative_base() Base = declarative_base()
def get_db_connection(): def get_db_session():
"""Retorna uma nova sessão do banco de dados com PRAGMAs configuradas""" """Retorna uma nova sessão do banco de dados com PRAGMAs configuradas"""
session = Session() db_session = Session()
try: try:
# Configurar SQLite para melhor tratamento de concorrência # Configurar SQLite para melhor tratamento de concorrência
session.execute(text("PRAGMA journal_mode=WAL")) db_session.execute(text("PRAGMA journal_mode=WAL"))
session.execute(text("PRAGMA busy_timeout=5000")) db_session.execute(text("PRAGMA busy_timeout=5000"))
return session return db_session
except Exception as e: except Exception as e:
session.rollback() db_session.rollback()
session.close() db_session.close()
raise e raise e

View File

@@ -1,10 +1,10 @@
from datetime import datetime, UTC from datetime import datetime, UTC
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from functions.database import get_db_connection, Controle as ControleModel from functions.database import get_db_session, Controle as ControleModel
class Controle: class Controle:
def __init__(self): def __init__(self):
self.db = get_db_connection() self.db = get_db_session()
def registrar_controle(self, militante_id: int, tipo: str, valor: float, observacao: str = None) -> bool: def registrar_controle(self, militante_id: int, tipo: str, valor: float, observacao: str = None) -> bool:
""" """

View File

@@ -1,8 +1,7 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta, UTC
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum
from sqlalchemy.orm import relationship, backref from sqlalchemy.orm import relationship, backref
import os
import pyotp import pyotp
import secrets import secrets
from flask_mail import Message from flask_mail import Message
@@ -10,23 +9,23 @@ from flask import url_for
import enum import enum
from flask_login import UserMixin from flask_login import UserMixin
from .rbac import Role from .rbac import Role
from .base import Base, engine, get_db_connection from .base import Base, get_db_session
def execute_query(query, params=None): def execute_query(query, params=None):
""" """
Executa uma query usando SQLAlchemy Executa uma query usando SQLAlchemy
""" """
session = get_db_connection() db = get_db_session()
try: try:
result = session.execute(query, params) result = db.execute(query, params)
session.commit() db.commit()
return result return result
except Exception as e: except Exception as e:
session.rollback() db.rollback()
raise e raise e
finally: finally:
session.close() db.close()
class EstadoMilitante(enum.Enum): class EstadoMilitante(enum.Enum):
ATIVO = 'ativo' ATIVO = 'ativo'
@@ -149,7 +148,7 @@ class Militante(Base):
quadro_orientador = Column(Boolean, default=False) quadro_orientador = Column(Boolean, default=False)
# Campos para Aspirante # Campos para Aspirante
aspirante = Column(Boolean, default=True) # Por padrão, todo novo militante é aspirante aspirante = Column(Boolean, default=True) # Por padrão, todo novo militante é aspirante
data_inicio_aspirante = Column(DateTime, default=datetime.utcnow) data_inicio_aspirante = Column(DateTime, default=datetime.now(UTC))
avaliacao_aspirante = Column(Text) avaliacao_aspirante = Column(Text)
data_avaliacao_aspirante = Column(DateTime) data_avaliacao_aspirante = Column(DateTime)
@@ -252,7 +251,7 @@ class Militante(Base):
def generate_username(self): def generate_username(self):
"""Gera um nome de usuário único baseado no primeiro nome e um código""" """Gera um nome de usuário único baseado no primeiro nome e um código"""
from sqlalchemy import func from sqlalchemy import func
db = get_db_connection() db = get_db_session()
try: try:
# Pega o primeiro nome # Pega o primeiro nome
primeiro_nome = self.nome.split()[0].lower() primeiro_nome = self.nome.split()[0].lower()
@@ -429,7 +428,7 @@ class Usuario(Base, UserMixin):
celula_id = Column(Integer, ForeignKey('celulas.id')) celula_id = Column(Integer, ForeignKey('celulas.id'))
session_timeout = Column(Integer, default=30) session_timeout = Column(Integer, default=30)
tipo = Column(String(17), nullable=False) tipo = Column(String(17), nullable=False)
ultima_atividade = Column(DateTime, default=datetime.utcnow) ultima_atividade = Column(DateTime, default=datetime.now(UTC))
# Relacionamento com militante # Relacionamento com militante
militante_id = Column(Integer, ForeignKey('militantes.id')) militante_id = Column(Integer, ForeignKey('militantes.id'))
militante = relationship("Militante", backref=backref("usuario", uselist=False)) militante = relationship("Militante", backref=backref("usuario", uselist=False))
@@ -448,7 +447,7 @@ class Usuario(Base, UserMixin):
self.ativo = True self.ativo = True
self.session_timeout = 30 self.session_timeout = 30
self.tipo = "USUARIO" self.tipo = "USUARIO"
self.ultima_atividade = datetime.utcnow() self.ultima_atividade = datetime.now(UTC)
def set_password(self, password): def set_password(self, password):
self.password_hash = generate_password_hash(password) self.password_hash = generate_password_hash(password)
@@ -457,23 +456,24 @@ class Usuario(Base, UserMixin):
return check_password_hash(self.password_hash, password) return check_password_hash(self.password_hash, password)
def update_last_activity(self): def update_last_activity(self):
self.ultima_atividade = datetime.utcnow() self.ultima_atividade = datetime.now(UTC)
def is_session_expired(self): def is_session_expired(self):
if not self.ultima_atividade: if not self.ultima_atividade:
return True return True
time_diff = datetime.utcnow() - self.ultima_atividade time_diff = datetime.now(UTC) - self.ultima_atividade
return time_diff.total_seconds() > (self.session_timeout * 60) return time_diff.total_seconds() > (self.session_timeout * 60)
def check_session_timeout(self): def check_session_timeout(self):
"""Verifica se a sessão do usuário expirou""" """Verifica se a sessão do usuário expirou"""
if not self.ultima_atividade: if not self.ultima_atividade:
return True return True
time_diff = datetime.utcnow() - self.ultima_atividade time_diff = datetime.now(UTC) - self.ultima_atividade
return time_diff.total_seconds() > (self.session_timeout * 60) return time_diff.total_seconds() > (self.session_timeout * 60)
def has_permission(self, permission_name): def has_permission(self, permission_name):
"""Verifica se o usuário tem uma permissão específica""" """Verifica se o usuário tem uma permissão específica"""
# TODO: (talvez) remover, confirmar admin por RBAC
if self.is_admin: # Se for admin, tem todas as permissões if self.is_admin: # Se for admin, tem todas as permissões
return True return True
@@ -485,54 +485,66 @@ class Usuario(Base, UserMixin):
return False return False
def has_role(self, role_nivel): def has_role(self, role_nivel):
"""Verifica se o usuário tem um determinado nível de role""" """Verifica se o usuário tem um nível de role específico."""
for role in self.roles: for role in self.roles:
if role.nivel == role_nivel: if role.nivel == role_nivel:
return True return True
return False return False
def get_otp_uri(self): def get_highest_role(self):
"""Gera a URI para autenticação em duas etapas""" """Retorna a role de maior nível do usuário."""
if not self.otp_secret: if not self.roles:
self.otp_secret = pyotp.random_base32() return None
return pyotp.totp.TOTP(self.otp_secret).provisioning_uri( return max(self.roles, key=lambda role: role.nivel)
self.username,
issuer_name="Sistema de Controles" def has_minimum_role(self, min_level):
) """Verifica se o usuário possui ao menos o nível informado."""
highest_role = self.get_highest_role()
return bool(highest_role and highest_role.nivel >= min_level)
def generate_otp_secret(self): def generate_otp_secret(self):
"""Gera um novo segredo OTP para o usuário""" """Gera um novo segredo OTP para o usuário"""
self.otp_secret = pyotp.random_base32() self.otp_secret = pyotp.random_base32()
return self.otp_secret return self.otp_secret
def get_otp_uri(self):
"""Gera a URI para autenticação em duas etapas"""
if not self.otp_secret:
raise ValueError(f"OTP não configurado para {self.username}")
totp = pyotp.TOTP(self.otp_secret)
return totp.provisioning_uri(
name=self.username,
issuer_name="Sistema de Controles"
)
def verify_otp(self, code): def verify_otp(self, code):
"""Verifica se um código OTP é válido""" """Verifica se um código OTP é válido"""
if not self.otp_secret: if not self.otp_secret:
print(f"Erro: OTP secret não configurado para o usuário {self.username}") raise ValueError(f"Erro: OTP secret não configurado para o usuário {self.username}")
return False
print(f"Verificando OTP para usuário {self.username}") print(f"Verificando OTP para usuário {self.username}")
print(f"OTP Secret: {self.otp_secret}") print(f"OTP Secret: {self.otp_secret}")
print(f"Código fornecido: {code}") print(f"Código fornecido: {code}")
totp = pyotp.totp.TOTP(self.otp_secret) totp = pyotp.TOTP(self.otp_secret)
is_valid = totp.verify(code) is_valid = totp.verify(code)
print(f"Resultado da verificação: {'Válido' if is_valid else 'Inválido'}") print(f"Resultado da verificação: {'Válido' if is_valid else 'Inválido'}")
print(f"Tempo atual: {datetime.utcnow()}") print(f"Tempo atual: {datetime.now(UTC)}")
print(f"Período atual: {totp.timecode(datetime.utcnow())}") print(f"Período atual: {totp.timecode(datetime.now(UTC))}")
return is_valid return is_valid
def logout(self): def logout(self):
"""Registra o logout do usuário""" """Registra o logout do usuário"""
self.ultimo_logout = datetime.utcnow() self.ultimo_logout = datetime.now(UTC)
self.motivo_logout = "Logout manual" self.motivo_logout = "Logout manual"
self.ultima_atividade = None self.ultima_atividade = None
def is_admin_user(self): def is_admin_user(self):
"""Verifica se o usuário é admin""" """Verifica se o usuário é admin"""
return self.is_admin or any(role.nome == "admin" for role in self.roles) return self.is_admin or any(role.nivel == Role.SECRETARIO_GERAL for role in self.roles)
class PagamentoCelula(Base): class PagamentoCelula(Base):
__tablename__ = 'pagamentos_celula' __tablename__ = 'pagamentos_celula'
@@ -605,116 +617,3 @@ class TransacaoPIX(Base):
pagamento_id = Column(Integer, ForeignKey('pagamentos.id')) pagamento_id = Column(Integer, ForeignKey('pagamentos.id'))
pagamento = relationship("Pagamento", back_populates="transacoes_pix") pagamento = relationship("Pagamento", back_populates="transacoes_pix")
def init_database():
"""Inicializa o banco de dados com dados básicos"""
print("Inicializando banco de dados...")
session = get_db_connection()
try:
# Criar todas as tabelas
Base.metadata.drop_all(engine) # Remover todas as tabelas existentes
Base.metadata.create_all(engine)
# Criar roles padrão
roles = [
("Administrador", Role.SECRETARIO_GERAL),
("Secretário", Role.SECRETARIO_CELULA),
("Militante", Role.MILITANTE_BASICO)
]
for nome, nivel in roles:
if not session.query(Role).filter_by(nome=nome).first():
role = Role(nome=nome, nivel=nivel)
session.add(role)
session.commit()
# Criar setores padrão
setores = ["Setor 1", "Setor 2", "Setor 3"]
for nome in setores:
if not session.query(Setor).filter_by(nome=nome).first():
setor = Setor(nome=nome)
session.add(setor)
session.commit()
# Criar comitês padrão
comites = ["Comitê 1", "Comitê 2", "Comitê 3"]
for nome in comites:
if not session.query(ComiteCentral).filter_by(nome=nome).first():
comite = ComiteCentral(nome=nome)
session.add(comite)
session.commit()
# Gerar OTP para admin
admin_otp_secret = os.environ.get('ADMIN_OTP_SECRET') or pyotp.random_base32()
print(f"OTP do admin: {admin_otp_secret}")
# Criar usuário admin
admin_role = session.query(Role).filter_by(nome="Administrador").first()
setor = session.query(Setor).first()
admin = Usuario(
username="admin",
email="admin@example.com",
is_admin=True
)
admin.set_password("admin123")
admin.tipo = "ADMIN"
admin.otp_secret = admin_otp_secret
admin.roles.append(admin_role)
admin.setor = setor
session.add(admin)
session.commit()
# Gerar QR code
totp = pyotp.totp.TOTP(admin_otp_secret)
provisioning_uri = totp.provisioning_uri("admin", issuer_name="Sistema de Controles")
import qrcode
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(provisioning_uri)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
# Tentar salvar em diferentes locais
qr_paths = ['/tmp/admin_qr.png', 'admin_qr.png', '/app/admin_qr.png']
qr_saved = False
for qr_path in qr_paths:
try:
img.save(qr_path)
print(f"QR code salvo em {qr_path}")
qr_saved = True
break
except Exception as e:
print(f"Não foi possível salvar o QR code em {qr_path}: {e}")
continue
if not qr_saved:
print("AVISO: Não foi possível salvar o QR code em nenhum local")
print("O QR code pode ser gerado manualmente usando o URI OTP")
print("=== Usuário Admin Criado ===")
print(f"Username: admin")
print(f"Senha: admin123")
print(f"Email: {admin.email}")
print(f"OTP Secret: {admin_otp_secret}")
if qr_saved:
print(f"QR Code: {qr_path}")
print(f"URI OTP: {provisioning_uri}")
# Importar e executar o seed após criar todas as dependências
from seed_data import seed_database
print("\nPopulando banco de dados com dados de teste...")
seed_database()
print("Dados de teste criados com sucesso!")
except Exception as e:
print(f"Erro na inicialização do banco: {e}")
session.rollback()
raise
finally:
session.close()
if __name__ == "__main__":
init_database()

View File

@@ -2,7 +2,7 @@ from functools import wraps
from flask import session, redirect, url_for, flash from flask import session, redirect, url_for, flash
from flask_login import current_user, login_required from flask_login import current_user, login_required
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from .database import get_db_connection, Usuario, Role from .database import get_db_session, Usuario, Role
from .rbac import Permission from .rbac import Permission
def require_login(f): def require_login(f):
@@ -26,7 +26,7 @@ def require_permission(permission_name):
flash('Você precisa estar logado para acessar esta página.', 'error') flash('Você precisa estar logado para acessar esta página.', 'error')
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
db = get_db_connection() db = get_db_session()
try: try:
# Carregar o usuário com suas roles e permissões # Carregar o usuário com suas roles e permissões
user = db.query(Usuario).options( user = db.query(Usuario).options(
@@ -58,8 +58,11 @@ def require_permission(permission_name):
return decorated_function return decorated_function
return decorator return decorator
def require_role(role_name): def require_role(role_level):
"""Decorador para verificar se o usuário tem um papel específico""" """Decorador para verificar se o usuário tem um papel específico"""
if not isinstance(role_level, int):
raise TypeError("require_role espera um nível numérico (int), use a classe Role.")
def decorator(f): def decorator(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
@@ -67,10 +70,10 @@ def require_role(role_name):
flash('Você precisa estar logado para acessar esta página.', 'error') flash('Você precisa estar logado para acessar esta página.', 'error')
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
db = get_db_connection() db = get_db_session()
try: try:
user = db.query(Usuario).get(current_user.id) user = db.query(Usuario).get(current_user.id)
if not user or not user.has_role(role_name): if not user or not user.has_role(role_level):
flash('Você não tem permissão para acessar esta página.', 'error') flash('Você não tem permissão para acessar esta página.', 'error')
return redirect(url_for('index')) return redirect(url_for('index'))
@@ -86,6 +89,9 @@ def require_role(role_name):
def require_minimum_role(min_level): def require_minimum_role(min_level):
"""Decorador para verificar se o usuário tem um papel com nível mínimo""" """Decorador para verificar se o usuário tem um papel com nível mínimo"""
if not isinstance(min_level, int):
raise TypeError("require_minimum_role espera um nível numérico de role (int).")
def decorator(f): def decorator(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
@@ -93,15 +99,14 @@ def require_minimum_role(min_level):
flash('Você precisa estar logado para acessar esta página.', 'error') flash('Você precisa estar logado para acessar esta página.', 'error')
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
db = get_db_connection() db = get_db_session()
try: try:
user = db.query(Usuario).get(current_user.id) user = db.query(Usuario).get(current_user.id)
if not user: if not user:
flash('Usuário não encontrado.', 'error') flash('Usuário não encontrado.', 'error')
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
highest_role = user.get_highest_role() if not user.has_minimum_role(min_level):
if not highest_role or highest_role.nivel < min_level:
flash('Você não tem permissão para acessar esta página.', 'error') flash('Você não tem permissão para acessar esta página.', 'error')
return redirect(url_for('index')) return redirect(url_for('index'))
@@ -147,30 +152,41 @@ def require_instance_access(instance_type, instance_id):
flash('Por favor, faça login para acessar esta página.', 'error') flash('Por favor, faça login para acessar esta página.', 'error')
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
db = get_db_session()
try:
user = db.query(Usuario).options(
joinedload(Usuario.roles).joinedload(Role.permissions)
).get(current_user.id)
if not user:
flash('Usuário não encontrado.', 'error')
return redirect(url_for('auth.login'))
# Verificar acesso baseado na instância do usuário # Verificar acesso baseado na instância do usuário
if instance_type == 'celula': if instance_type == 'celula':
if not (current_user.celula_id == instance_id or if not (user.celula_id == instance_id or
current_user.has_permission(Permission.VIEW_SECTOR_REPORTS) or user.has_permission(Permission.VIEW_SECTOR_REPORTS) or
current_user.has_permission(Permission.VIEW_CR_REPORTS) or user.has_permission(Permission.VIEW_CR_REPORTS) or
current_user.has_permission(Permission.VIEW_CC_REPORTS)): user.has_permission(Permission.VIEW_CC_REPORTS)):
flash('Você não tem acesso a esta célula.', 'error') flash('Você não tem acesso a esta célula.', 'error')
return redirect(url_for('index')) return redirect(url_for('index'))
elif instance_type == 'setor': elif instance_type == 'setor':
if not (current_user.setor_id == instance_id or if not (user.setor_id == instance_id or
current_user.has_permission(Permission.VIEW_CR_REPORTS) or user.has_permission(Permission.VIEW_CR_REPORTS) or
current_user.has_permission(Permission.VIEW_CC_REPORTS)): user.has_permission(Permission.VIEW_CC_REPORTS)):
flash('Você não tem acesso a este setor.', 'error') flash('Você não tem acesso a este setor.', 'error')
return redirect(url_for('index')) return redirect(url_for('index'))
elif instance_type == 'cr': elif instance_type == 'cr':
if not (current_user.cr_id == instance_id or if not (user.cr_id == instance_id or
current_user.has_permission(Permission.VIEW_CC_REPORTS)): user.has_permission(Permission.VIEW_CC_REPORTS)):
flash('Você não tem acesso a este CR.', 'error') flash('Você não tem acesso a este CR.', 'error')
return redirect(url_for('index')) return redirect(url_for('index'))
# Atualiza timestamp da última atividade # Atualiza timestamp da última atividade
current_user.update_last_activity() user.update_last_activity()
db_session.commit() db.commit()
return f(*args, **kwargs) return f(*args, **kwargs)
finally:
db.close()
return decorated_function return decorated_function
return decorator return decorator

View File

@@ -133,183 +133,183 @@ class Permission(Base):
def init_rbac(): def init_rbac():
"""Inicializa o sistema RBAC com roles e permissões básicas""" """Inicializa o sistema RBAC com roles e permissões básicas"""
from .database import Usuario, get_db_connection from .database import Usuario, get_db_session
session = get_db_connection() db = get_db_session()
try: try:
# Criar role de administrador primeiro # Criar role de administrador primeiro
admin_role = session.query(Role).filter_by(nome="Administrador").first() admin_role = db.query(Role).filter_by(nome="Administrador").first()
if not admin_role: if not admin_role:
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL) admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
session.add(admin_role) db.add(admin_role)
session.commit() db.commit()
# Criar outras roles # Criar outras roles
for nivel, nome in Role.get_roles_list(): for nivel, nome in Role.get_roles_list():
if nome != "Administrador": # Pular Administrador pois já foi criado if nome != "Administrador": # Pular Administrador pois já foi criado
role = session.query(Role).filter_by(nivel=nivel).first() role = db.query(Role).filter_by(nivel=nivel).first()
if not role: if not role:
role = Role(nome=nome, nivel=nivel) role = Role(nome=nome, nivel=nivel)
session.add(role) db.add(role)
# Criar permissões # Criar permissões
for nome, descricao in Permission.get_permissions_list(): for nome, descricao in Permission.get_permissions_list():
permission = session.query(Permission).filter_by(nome=nome).first() permission = db.query(Permission).filter_by(nome=nome).first()
if not permission: if not permission:
permission = Permission(nome=nome, descricao=descricao) permission = Permission(nome=nome, descricao=descricao)
session.add(permission) db.add(permission)
session.commit() db.commit()
# Dar todas as permissões para o admin # Dar todas as permissões para o admin
all_permissions = session.query(Permission).all() all_permissions = db.query(Permission).all()
admin_role.permissions = all_permissions admin_role.permissions = all_permissions
session.commit() db.commit()
# Buscar usuário admin e atribuir role de administrador # Buscar usuário admin e atribuir role de administrador
admin_user = session.query(Usuario).filter_by(username="admin").first() admin_user = db.query(Usuario).filter_by(username="admin").first()
if admin_user: if admin_user:
if admin_role not in admin_user.roles: if admin_role not in admin_user.roles:
admin_user.roles = [admin_role] # Substituir roles existentes admin_user.roles = [admin_role] # Substituir roles existentes
session.commit() db.commit()
# Mapear permissões para outros roles # Mapear permissões para outros roles
for role in session.query(Role).filter(Role.nome != "Administrador").all(): for role in db.query(Role).filter(Role.nome != "Administrador").all():
# Militante Básico # Militante Básico
if role.nivel == Role.MILITANTE_BASICO: if role.nivel == Role.MILITANTE_BASICO:
role.permissions = [ role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(), db.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(), db.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first() db.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first()
] ]
# Secretário de Célula # Secretário de Célula
elif role.nivel == Role.SECRETARIO_CELULA: elif role.nivel == Role.SECRETARIO_CELULA:
role.permissions = [ role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(), db.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(), db.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(), db.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_CELL_PAYMENT).first() db.query(Permission).filter_by(nome=Permission.REGISTER_CELL_PAYMENT).first()
] ]
# Membro de Setor # Membro de Setor
elif role.nivel == Role.MEMBRO_SETOR: elif role.nivel == Role.MEMBRO_SETOR:
role.permissions = [ role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(), db.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(), db.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(), db.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first() db.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first()
] ]
# Secretário de Setor # Secretário de Setor
elif role.nivel == Role.SECRETARIO_SETOR: elif role.nivel == Role.SECRETARIO_SETOR:
role.permissions = [ role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(), db.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(), db.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(), db.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(), db.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first() db.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first()
] ]
# Membro de CR # Membro de CR
elif role.nivel == Role.MEMBRO_CR: elif role.nivel == Role.MEMBRO_CR:
role.permissions = [ role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(), db.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(), db.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(), db.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(), db.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first() db.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first()
] ]
# Secretário de CR # Secretário de CR
elif role.nivel == Role.SECRETARIO_CR: elif role.nivel == Role.SECRETARIO_CR:
role.permissions = [ role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(), db.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(), db.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(), db.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(), db.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(), db.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first() db.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first()
] ]
# Membro do CC # Membro do CC
elif role.nivel == Role.MEMBRO_CC: elif role.nivel == Role.MEMBRO_CC:
role.permissions = [ role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(), db.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(), db.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(), db.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(), db.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(), db.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first() db.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first()
] ]
# Secretário Geral # Secretário Geral
elif role.nivel == Role.SECRETARIO_GERAL: elif role.nivel == Role.SECRETARIO_GERAL:
role.permissions = [ role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(), db.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(), db.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(), db.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(), db.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(), db.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(), db.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CC_CRS).first(), db.query(Permission).filter_by(nome=Permission.MANAGE_CC_CRS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CC_CR).first(), db.query(Permission).filter_by(nome=Permission.CREATE_CC_CR).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first(), db.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first(),
session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first() db.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first()
] ]
session.commit() db.commit()
except Exception as e: except Exception as e:
print(f"Erro ao inicializar RBAC: {e}") print(f"Erro ao inicializar RBAC: {e}")
session.rollback() db.rollback()
raise raise
finally: finally:
session.close() db.close()

View File

@@ -1,19 +0,0 @@
from functions.database import init_database
from functions.rbac import init_rbac
from create_admin import create_admin_user
from create_test_users import create_test_users
def init_system():
print("Inicializando banco de dados...")
init_database()
print("Inicializando sistema RBAC...")
init_rbac()
print("Criando usuários iniciais...")
create_admin_user()
create_test_users()
if __name__ == "__main__":
init_system()
print("Sistema inicializado com sucesso!")

View File

@@ -1,58 +0,0 @@
from create_admin import create_admin
from create_test_users import create_test_users
from functions.database import get_db_connection, Usuario
from functions.rbac import Role
def init_system():
print("=== Inicializando Sistema ===")
# Criar admin
print("\nCriando usuário admin...")
create_admin()
# Criar usuários de teste
print("\nCriando usuários de teste...")
create_test_users()
# Verificar configuração
print("\n=== Verificando Configuração ===")
session = get_db_connection()
try:
# Verificar admin
admin = session.query(Usuario).filter_by(username='admin').first()
if admin:
print("Admin: OK")
print(f"OTP configurado: {'Sim' if admin.otp_secret else 'Não'}")
else:
print("Admin: FALHOU")
# Verificar usuários de teste
test_users = ['aligner', 'tester', 'deployer']
for username in test_users:
user = session.query(Usuario).filter_by(username=username).first()
if user:
print(f"{username}: OK")
print(f"OTP configurado: {'Sim' if user.otp_secret else 'Não'}")
else:
print(f"{username}: FALHOU")
print("\n=== Instruções ===")
print("1. Use o aplicativo autenticador para configurar o OTP de cada usuário")
print("2. Faça login com cada usuário para testar")
print("3. Altere a senha no primeiro login")
print("\nCredenciais:")
print("Admin:")
print(" Usuário: admin")
print(" Senha: admin123")
print("\nUsuários de teste:")
print(" Usuário: aligner, tester, deployer")
print(" Senha: Test123!@#")
except Exception as e:
print(f"Erro ao verificar configuração: {str(e)}")
session.rollback()
finally:
session.close()
if __name__ == "__main__":
init_system()

View File

@@ -1,4 +1,4 @@
from functions.database import get_db_connection, Militante, EmailMilitante, Endereco from functions.database import get_db_session, Militante, EmailMilitante, Endereco
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from datetime import datetime from datetime import datetime
from typing import List, Dict, Optional from typing import List, Dict, Optional
@@ -14,7 +14,7 @@ class MilitanteModel:
@invalidate_cache_pattern("militantes:*") @invalidate_cache_pattern("militantes:*")
def criar_militante(data: Dict) -> Dict: def criar_militante(data: Dict) -> Dict:
"""Cria um novo militante""" """Cria um novo militante"""
db = get_db_connection() db = get_db_session()
try: try:
# Criar endereço se fornecido # Criar endereço se fornecido
endereco_id = None endereco_id = None
@@ -89,7 +89,7 @@ class MilitanteModel:
@cached(expire=1800, key_prefix="militantes") # Cache for 30 minutes @cached(expire=1800, key_prefix="militantes") # Cache for 30 minutes
def listar_militantes() -> List[Militante]: def listar_militantes() -> List[Militante]:
"""Lista todos os militantes""" """Lista todos os militantes"""
db = get_db_connection() db = get_db_session()
try: try:
militantes = db.query(Militante).options( militantes = db.query(Militante).options(
joinedload(Militante.emails), joinedload(Militante.emails),
@@ -125,7 +125,7 @@ class MilitanteModel:
return cached_militante return cached_militante
# Cache miss, get from database # Cache miss, get from database
db = get_db_connection() db = get_db_session()
try: try:
militante = db.query(Militante).options( militante = db.query(Militante).options(
joinedload(Militante.emails), joinedload(Militante.emails),
@@ -150,7 +150,7 @@ class MilitanteModel:
@invalidate_cache_pattern("militantes:*") @invalidate_cache_pattern("militantes:*")
def atualizar_militante(militante_id: int, data: Dict) -> Dict: def atualizar_militante(militante_id: int, data: Dict) -> Dict:
"""Atualiza um militante existente""" """Atualiza um militante existente"""
db = get_db_connection() db = get_db_session()
try: try:
militante = db.query(Militante).get(militante_id) militante = db.query(Militante).get(militante_id)
@@ -228,7 +228,7 @@ class MilitanteModel:
@invalidate_cache_pattern("militantes:*") @invalidate_cache_pattern("militantes:*")
def excluir_militante(militante_id: int) -> Dict: def excluir_militante(militante_id: int) -> Dict:
"""Exclui um militante""" """Exclui um militante"""
db = get_db_connection() db = get_db_session()
try: try:
militante = db.query(Militante).get(militante_id) militante = db.query(Militante).get(militante_id)
if not militante: if not militante:
@@ -265,7 +265,7 @@ class MilitanteModel:
@cached(expire=1800, key_prefix="militantes") @cached(expire=1800, key_prefix="militantes")
def buscar_por_cpf(cpf: str) -> Optional[Militante]: def buscar_por_cpf(cpf: str) -> Optional[Militante]:
"""Busca um militante por CPF""" """Busca um militante por CPF"""
db = get_db_connection() db = get_db_session()
try: try:
militante = db.query(Militante).filter_by(cpf=cpf).first() militante = db.query(Militante).filter_by(cpf=cpf).first()
if militante: if militante:

View File

@@ -1,4 +1,4 @@
from functions.database import get_db_connection, Pagamento, Militante, TipoPagamento from functions.database import get_db_session, Pagamento, Militante, TipoPagamento
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from datetime import datetime from datetime import datetime
from typing import List, Dict, Optional from typing import List, Dict, Optional
@@ -9,7 +9,7 @@ class PagamentoModel:
@staticmethod @staticmethod
def criar_pagamento(data: Dict) -> Dict: def criar_pagamento(data: Dict) -> Dict:
"""Cria um novo pagamento""" """Cria um novo pagamento"""
db = get_db_connection() db = get_db_session()
try: try:
pagamento = Pagamento( pagamento = Pagamento(
militante_id=data['militante_id'], militante_id=data['militante_id'],
@@ -38,7 +38,7 @@ class PagamentoModel:
@staticmethod @staticmethod
def listar_pagamentos() -> List[Pagamento]: def listar_pagamentos() -> List[Pagamento]:
"""Lista todos os pagamentos""" """Lista todos os pagamentos"""
db = get_db_connection() db = get_db_session()
try: try:
return db.query(Pagamento).join(Militante).order_by(Pagamento.data_pagamento.desc()).all() return db.query(Pagamento).join(Militante).order_by(Pagamento.data_pagamento.desc()).all()
finally: finally:
@@ -47,7 +47,7 @@ class PagamentoModel:
@staticmethod @staticmethod
def buscar_por_id(pagamento_id: int) -> Optional[Pagamento]: def buscar_por_id(pagamento_id: int) -> Optional[Pagamento]:
"""Busca um pagamento por ID""" """Busca um pagamento por ID"""
db = get_db_connection() db = get_db_session()
try: try:
return db.query(Pagamento).get(pagamento_id) return db.query(Pagamento).get(pagamento_id)
finally: finally:
@@ -56,7 +56,7 @@ class PagamentoModel:
@staticmethod @staticmethod
def atualizar_pagamento(pagamento_id: int, data: Dict) -> Dict: def atualizar_pagamento(pagamento_id: int, data: Dict) -> Dict:
"""Atualiza um pagamento existente""" """Atualiza um pagamento existente"""
db = get_db_connection() db = get_db_session()
try: try:
pagamento = db.query(Pagamento).get(pagamento_id) pagamento = db.query(Pagamento).get(pagamento_id)
@@ -90,7 +90,7 @@ class PagamentoModel:
@staticmethod @staticmethod
def excluir_pagamento(pagamento_id: int) -> Dict: def excluir_pagamento(pagamento_id: int) -> Dict:
"""Exclui um pagamento""" """Exclui um pagamento"""
db = get_db_connection() db = get_db_session()
try: try:
pagamento = db.query(Pagamento).get(pagamento_id) pagamento = db.query(Pagamento).get(pagamento_id)
if not pagamento: if not pagamento:
@@ -119,7 +119,7 @@ class PagamentoModel:
@staticmethod @staticmethod
def listar_por_celula(celula_id: int) -> List[Pagamento]: def listar_por_celula(celula_id: int) -> List[Pagamento]:
"""Lista pagamentos de uma célula específica""" """Lista pagamentos de uma célula específica"""
db = get_db_connection() db = get_db_session()
try: try:
return db.query(Pagamento).filter_by(celula_id=celula_id).all() return db.query(Pagamento).filter_by(celula_id=celula_id).all()
finally: finally:
@@ -128,7 +128,7 @@ class PagamentoModel:
@staticmethod @staticmethod
def listar_por_setor(setor_id: int) -> List[Pagamento]: def listar_por_setor(setor_id: int) -> List[Pagamento]:
"""Lista pagamentos de um setor específico""" """Lista pagamentos de um setor específico"""
db = get_db_connection() db = get_db_session()
try: try:
return db.query(Pagamento).join(Usuario).filter(Usuario.setor_id == setor_id).all() return db.query(Pagamento).join(Usuario).filter(Usuario.setor_id == setor_id).all()
finally: finally:
@@ -137,7 +137,7 @@ class PagamentoModel:
@staticmethod @staticmethod
def listar_por_cr(cr_id: int) -> List[Pagamento]: def listar_por_cr(cr_id: int) -> List[Pagamento]:
"""Lista pagamentos de um CR específico""" """Lista pagamentos de um CR específico"""
db = get_db_connection() db = get_db_session()
try: try:
return db.query(Pagamento).join(Usuario).filter(Usuario.cr_id == cr_id).all() return db.query(Pagamento).join(Usuario).filter(Usuario.cr_id == cr_id).all()
finally: finally:
@@ -146,7 +146,7 @@ class PagamentoModel:
@staticmethod @staticmethod
def listar_por_militante(militante_id: int) -> List[Pagamento]: def listar_por_militante(militante_id: int) -> List[Pagamento]:
"""Lista pagamentos de um militante específico""" """Lista pagamentos de um militante específico"""
db = get_db_connection() db = get_db_session()
try: try:
return db.query(Pagamento).filter_by(militante_id=militante_id).order_by(Pagamento.data_pagamento.desc()).all() return db.query(Pagamento).filter_by(militante_id=militante_id).order_by(Pagamento.data_pagamento.desc()).all()
finally: finally:
@@ -155,7 +155,7 @@ class PagamentoModel:
@staticmethod @staticmethod
def obter_tipos_pagamento() -> List[TipoPagamento]: def obter_tipos_pagamento() -> List[TipoPagamento]:
"""Obtém todos os tipos de pagamento""" """Obtém todos os tipos de pagamento"""
db = get_db_connection() db = get_db_session()
try: try:
return db.query(TipoPagamento).order_by(TipoPagamento.descricao).all() return db.query(TipoPagamento).order_by(TipoPagamento.descricao).all()
finally: finally:
@@ -164,7 +164,7 @@ class PagamentoModel:
@staticmethod @staticmethod
def obter_militantes() -> List[Militante]: def obter_militantes() -> List[Militante]:
"""Obtém todos os militantes""" """Obtém todos os militantes"""
db = get_db_connection() db = get_db_session()
try: try:
return db.query(Militante).order_by(Militante.nome).all() return db.query(Militante).order_by(Militante.nome).all()
finally: finally:

View File

@@ -1,5 +1,5 @@
from flask import Blueprint, render_template, flash, redirect, url_for, request, jsonify from flask import Blueprint, render_template, flash, redirect, url_for, request, jsonify
from functions.database import Usuario, get_db_connection from functions.database import Usuario, get_db_session
from functions.decorators import require_login from functions.decorators import require_login
from flask_login import login_required, current_user from flask_login import login_required, current_user
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
@@ -29,7 +29,7 @@ def admin_required(f):
@admin_required @admin_required
def dashboard(): def dashboard():
"""Dashboard principal da área administrativa com lista de usuários""" """Dashboard principal da área administrativa com lista de usuários"""
db = get_db_connection() db = get_db_session()
try: try:
now = datetime.now() now = datetime.now()
@@ -68,7 +68,7 @@ def dashboard():
@admin_required @admin_required
def reset_user_otp(user_id): def reset_user_otp(user_id):
"""Reseta o OTP de um usuário""" """Reseta o OTP de um usuário"""
db = get_db_connection() db = get_db_session()
try: try:
user = db.query(Usuario).get(user_id) user = db.query(Usuario).get(user_id)
if not user: if not user:
@@ -89,7 +89,7 @@ def reset_user_otp(user_id):
@admin_required @admin_required
def reset_user_password(user_id): def reset_user_password(user_id):
"""Reseta a senha de um usuário""" """Reseta a senha de um usuário"""
db = get_db_connection() db = get_db_session()
try: try:
user = db.query(Usuario).get(user_id) user = db.query(Usuario).get(user_id)
if not user: if not user:
@@ -111,7 +111,7 @@ def reset_user_password(user_id):
@admin_required @admin_required
def toggle_user_status(user_id): def toggle_user_status(user_id):
"""Ativa/desativa um usuário""" """Ativa/desativa um usuário"""
db = get_db_connection() db = get_db_session()
try: try:
user = db.query(Usuario).get(user_id) user = db.query(Usuario).get(user_id)
if not user: if not user:

198
scripts/create_admin.py Normal file
View File

@@ -0,0 +1,198 @@
import os
import pyotp
from pathlib import Path
from functions.database import Usuario, Role, get_db_session
from services.otp_service import generate_qr_code
ADMIN_USERNAME = "admin"
ADMIN_PASSWORD = "admin123"
ADMIN_ROLE = Role.SECRETARIO_GERAL
def salvar_qr_code(user):
"""
Gera o QR code para um usuário específico
Args:
user: Instância do modelo Usuario
Returns:
tuple: (caminho do arquivo, URI do OTP)
"""
# Tentar diferentes caminhos para salvar o QR code
qr_paths = [
Path('/tmp/admin_qr.png'), # Diretório temporário do sistema
Path('/data/admin_qr.png'), # Diretório de dados do container
Path('admin_qr.png') # Diretório atual (fallback fora do container)
]
# Tentar salvar em diferentes locais
qr_saved = False
saved_path = None
img = generate_qr_code(user) # Gera o QR code para o usuário
for qr_path in qr_paths:
try:
# Tentar salvar o arquivo
img.save(qr_path)
qr_saved = True
saved_path = qr_path
break
except Exception as e:
print(f"Não foi possível salvar o QR code em {qr_path}: {e}")
continue
if not qr_saved:
print("AVISO: Não foi possível salvar o QR code em nenhum local")
print("O QR code pode ser gerado manualmente usando o URI OTP")
saved_path = None
return saved_path
def _ensure_admin_role(db, admin_user, role):
admin_role = db.query(Role).filter_by(nome=role).first()
if admin_role is None:
admin_role = Role(nome=role, nivel=Role.SECRETARIO_GERAL)
db.add(admin_role)
db.flush()
if admin_role not in admin_user.roles:
admin_user.roles.append(admin_role)
db.flush()
def _ensure_admin_otp(db, admin_user):
if admin_user.otp_secret:
return False
secret = (os.environ.get('ADMIN_OTP_SECRET') or "").strip()
admin_user.otp_secret = secret or admin_user.generate_otp_secret()
db.flush()
return True
def create_admin(username=ADMIN_USERNAME, password=ADMIN_PASSWORD, role=ADMIN_ROLE, save_qr=True):
"""Limpa e cria o usuário admin"""
db = get_db_session()
try:
# Verificar se já existe um usuário admin
admin_user = db.query(Usuario).filter_by(username=username).first()
if admin_user is not None:
db.delete(admin_user)
db.flush()
print("\n=== Criando Novo Usuário Admin ===")
admin_user = Usuario(
username=username,
email="admin@example.com",
is_admin=True
)
admin_user.set_password(password)
_ensure_admin_otp(db, admin_user)
_ensure_admin_role(db, admin_user, role)
db.add(admin_user)
db.commit()
qr_path = salvar_qr_code(admin_user)
# Mostrar informações
print("\n=== Informações do Admin ===")
print(f"Username: {admin_user.username}")
print(f"Email: {admin_user.email}")
print(f"Senha: {password}")
print(f"Segredo OTP: {admin_user.otp_secret}")
print(f"URI do OTP: {admin_user.get_otp_uri()}")
if qr_path:
print(f"QR Code salvo em: {qr_path}")
else:
print("QR Code não foi salvo. Use o URI do OTP ou o Segredo OTP para configuração manual.")
print("\n=== Instruções para Configuração ===")
print("1. Instale um aplicativo autenticador no seu celular")
print(" (Google Authenticator, Microsoft Authenticator, etc)")
print("2. Abra o aplicativo")
print("3. Selecione a opção para adicionar uma nova conta")
if qr_path:
print("4. Escaneie o QR Code salvo em:", qr_path)
print("\nOU configure manualmente:")
print(f"- Nome da conta: {admin_user.username}")
print(f"- Segredo OTP: {admin_user.otp_secret}")
print("- Tipo: Baseado em tempo (TOTP)")
print("- Algoritmo: SHA1")
print("- Dígitos: 6")
print("- Intervalo: 30 segundos")
# Gerar código atual para verificação
totp = pyotp.TOTP(admin_user.otp_secret)
current_code = totp.now()
print("\n=== Verificação do OTP ===")
print(f"Código OTP atual: {current_code}")
is_valid = admin_user.verify_otp(current_code)
print(f"Verificação do código: {'Sucesso' if is_valid else 'Falha'}")
if not is_valid:
print("\nALERTA: Verificação do OTP falhou!")
print("Por favor, verifique se o segredo OTP está correto.")
# Fazer commit final para garantir que tudo foi salvo
db.commit()
except Exception as e:
db.rollback()
raise e
finally:
db.close()
def verify_admin(username=ADMIN_USERNAME, role=ADMIN_ROLE, save_qr=False):
"""Verifica se o usuário admin existe e tem OTP configurado"""
db = get_db_session()
try:
admin_user = db.query(Usuario).filter_by(username=username).first()
if admin_user is not None:
print("\n=== Usuário Admin Encontrado ===")
_ensure_admin_otp(db, admin_user)
_ensure_admin_role(db, admin_user, role)
return True
else:
print("\n=== Usuário Admin NÃO Encontrado ===")
return False
except Exception as e:
print(f"Erro ao verificar o usuário admin: {e}")
raise
finally:
db.close()
def rotate_admin_otp(username=ADMIN_USERNAME, save_qr=False):
db = get_db_session()
try:
admin_user = db.query(Usuario).filter_by(username=username).first()
if admin_user is None:
print("Usuário admin não encontrado")
return False
admin_user.generate_otp_secret()
db.commit()
print(f"OTP do usuário '{username}' foi rotacionado.")
print(f"Novo segredo OTP: {admin_user.otp_secret}")
if save_qr:
qr_path = salvar_qr_code(admin_user)
if qr_path:
print(f"Novo QR code salvo em: {qr_path}")
else:
print("Não foi possível salvar o QR code automaticamente.")
except Exception:
db.rollback()
raise
finally:
db.close()
if __name__ == "__main__":
create_admin()

View File

@@ -1,9 +1,9 @@
from functions.database import get_db_connection, Usuario, Role from functions.database import get_db_session, Usuario, Role
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
def create_test_users(): def create_test_users():
"""Cria usuários de teste""" """Cria usuários de teste"""
db = get_db_connection() db = get_db_session()
try: try:
# Lista de usuários de teste # Lista de usuários de teste
test_users = [ test_users = [

View File

@@ -1,27 +0,0 @@
from functions.database import Role, Permissao, RolePermissao, Base, engine
from sqlalchemy.orm import Session
def init_db():
Base.metadata.create_all(engine)
with Session(engine) as session:
# Criar roles
admin = Role(nome='Administrador', nivel=1)
coord = Role(nome='Coordenador', nivel=2)
milit = Role(nome='Militante', nivel=3)
# Criar permissões
perm_admin = Permissao(nome='admin', descricao='Acesso total')
perm_militantes = Permissao(nome='ver_militantes', descricao='Ver militantes')
# ... outras permissões ...
session.add_all([admin, coord, milit, perm_admin, perm_militantes])
session.commit()
# Associar permissões aos roles
session.add(RolePermissao(role=admin, permissao=perm_admin))
session.add(RolePermissao(role=coord, permissao=perm_militantes))
session.commit()
if __name__ == '__main__':
init_db()

92
scripts/manage.py Normal file
View File

@@ -0,0 +1,92 @@
import argparse
import sys
from pathlib import Path
from dotenv import load_dotenv
ROOT_DIR = Path(__file__).resolve().parents[1]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
# Carregar .env antes de importar módulos
load_dotenv(ROOT_DIR / ".env")
from functions.base import Base, engine, get_db_session
from functions.rbac import Role, init_rbac
from scripts.create_admin import create_admin, rotate_admin_otp
from scripts.create_test_users import create_test_users
from scripts.seed_database import seed_database
ADMIN_USERNAME = "admin"
ADMIN_PASSWORD = "admin123"
ADMIN_ROLE = Role.SECRETARIO_GERAL
def reset_db(args):
"""Inicializando banco de dados e criando tabelas"""
db = get_db_session()
try:
# Criar todas as tabelas
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
except Exception as e:
print(f"Erro na drop ou create all da Base: {e}")
db.rollback()
raise
finally:
db.close()
print("Inicializando sistema RBAC...")
init_rbac()
print("Cria usuário admin...")
create_admin(username=ADMIN_USERNAME, password=ADMIN_PASSWORD, role=ADMIN_ROLE)
print("Banco inicializado com sucesso.")
return 0
def seed_db_with_fakes(args):
"""Função para popular o banco com dados fake para desenvolvimento"""
seed_database()
def seed_db_test_users(args):
"""Função para popular o banco com dados fake para desenvolvimento"""
create_test_users()
def reset_admin(args):
create_admin(username=ADMIN_USERNAME, password=ADMIN_PASSWORD, role=ADMIN_ROLE)
def rotate_admin_otp_cmd(args):
rotate_admin_otp(username=ADMIN_USERNAME, save_qr=True)
def build_parser():
parser = argparse.ArgumentParser(description="Gerenciador de comandos do sistema Controles")
subparsers = parser.add_subparsers(dest="command", required=True)
db_reset_parser = subparsers.add_parser("db_reset", help="Reseta o banco e recria tabelas, RBAC e admin")
db_reset_parser.set_defaults(func=reset_db)
db_seed_fake_parser = subparsers.add_parser("db_seed_fake", help="Adiciona dados falsos para desenvolvimento")
db_seed_fake_parser.set_defaults(func=seed_db_with_fakes)
db_seed_test_users_parser = subparsers.add_parser("db_seed_test_users", help="Adiciona usuários de teste para desenvolvimento")
db_seed_test_users_parser.set_defaults(func=seed_db_test_users)
admin_reset_parser = subparsers.add_parser("admin_reset", help="Reseta o usuário admin (padrão: admin123)")
admin_reset_parser.set_defaults(func=reset_admin)
admin_rotate_otp_parser = subparsers.add_parser("admin_rotate_otp", help="Rotaciona o OTP do usuário admin - se não definido em .env")
admin_rotate_otp_parser.set_defaults(func=rotate_admin_otp_cmd)
return parser
def main():
parser = build_parser()
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -4,30 +4,29 @@ from functions.database import (
MaterialVendido, TipoMaterial, VendaJornalAvulso, AssinaturaAnual, MaterialVendido, TipoMaterial, VendaJornalAvulso, AssinaturaAnual,
RelatorioCotasMensais, RelatorioVendasMateriais, RelatorioCotasMensais, RelatorioVendasMateriais,
Setor, ComiteCentral, Usuario, Role, EmailMilitante, Endereco, Setor, ComiteCentral, Usuario, Role, EmailMilitante, Endereco,
ComiteRegional, Celula, EstadoMilitante, get_db_connection ComiteRegional, Celula, EstadoMilitante, get_db_session
) )
import random import random
from faker import Faker from faker import Faker
import time
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
fake = Faker('pt_BR') fake = Faker('pt_BR')
def criar_estrutura_organizacional(session): def criar_estrutura_organizacional(db):
"""Cria a estrutura organizacional básica""" """Cria a estrutura organizacional básica"""
print("\nCriando estrutura organizacional...") print("\nCriando estrutura organizacional...")
# Criar Comitê Central # Criar Comitê Central
cc = ComiteCentral(nome="Comitê Central SP") cc = ComiteCentral(nome="Comitê Central SP")
session.add(cc) db.add(cc)
session.flush() db.flush()
# Criar Comitês Regionais # Criar Comitês Regionais
crs = [] crs = []
for nome in ["CR São Paulo", "CR ABC", "CR Campinas"]: for nome in ["CR São Paulo", "CR ABC", "CR Campinas"]:
cr = ComiteRegional(nome=nome) cr = ComiteRegional(nome=nome)
session.add(cr) db.add(cr)
session.flush() db.flush()
crs.append(cr) crs.append(cr)
# Criar Setores para cada CR # Criar Setores para cada CR
@@ -38,8 +37,8 @@ def criar_estrutura_organizacional(session):
nome=f"Setor {i+1} - {cr.nome}", nome=f"Setor {i+1} - {cr.nome}",
cr_id=cr.id cr_id=cr.id
) )
session.add(setor) db.add(setor)
session.flush() db.flush()
setores.append(setor) setores.append(setor)
# Criar Células para cada Setor # Criar Células para cada Setor
@@ -49,12 +48,12 @@ def criar_estrutura_organizacional(session):
nome=f"Célula {i+1} - {setor.nome}", nome=f"Célula {i+1} - {setor.nome}",
setor_id=setor.id setor_id=setor.id
) )
session.add(celula) db.add(celula)
session.commit() db.commit()
return crs, setores return crs, setores
def criar_tipos_pagamento(session): def criar_tipos_pagamento(db):
"""Cria tipos de pagamento padrão""" """Cria tipos de pagamento padrão"""
print("\nCriando tipos de pagamento...") print("\nCriando tipos de pagamento...")
tipos = [ tipos = [
@@ -65,11 +64,11 @@ def criar_tipos_pagamento(session):
"Transferência Bancária" "Transferência Bancária"
] ]
for tipo in tipos: for tipo in tipos:
if not session.query(TipoPagamento).filter_by(descricao=tipo).first(): if not db.query(TipoPagamento).filter_by(descricao=tipo).first():
session.add(TipoPagamento(descricao=tipo)) db.add(TipoPagamento(descricao=tipo))
session.commit() db.commit()
def criar_tipos_material(session): def criar_tipos_material(db):
"""Cria tipos de material padrão""" """Cria tipos de material padrão"""
print("\nCriando tipos de material...") print("\nCriando tipos de material...")
tipos = [ tipos = [
@@ -80,11 +79,11 @@ def criar_tipos_material(session):
"Cartilha" "Cartilha"
] ]
for tipo in tipos: for tipo in tipos:
if not session.query(TipoMaterial).filter_by(descricao=tipo).first(): if not db.query(TipoMaterial).filter_by(descricao=tipo).first():
session.add(TipoMaterial(descricao=tipo)) db.add(TipoMaterial(descricao=tipo))
session.commit() db.commit()
def criar_militantes(session, num_militantes, setores): def criar_militantes(db, num_militantes, setores):
"""Cria militantes com todos os dados necessários""" """Cria militantes com todos os dados necessários"""
print(f"\nCriando {num_militantes} militantes...") print(f"\nCriando {num_militantes} militantes...")
militantes = [] militantes = []
@@ -96,13 +95,6 @@ def criar_militantes(session, num_militantes, setores):
nome = fake.name() nome = fake.name()
cpf = fake.cpf() cpf = fake.cpf()
# Email único
while True:
email = fake.email()
if email not in emails_usados:
emails_usados.add(email)
break
# Criar endereço # Criar endereço
endereco = Endereco( endereco = Endereco(
cep=fake.postcode(), cep=fake.postcode(),
@@ -113,12 +105,12 @@ def criar_militantes(session, num_militantes, setores):
numero=str(random.randint(1, 999)), numero=str(random.randint(1, 999)),
complemento=f"Bloco {random.randint(1, 10)}, Apto {random.randint(1, 999)}" if random.random() < 0.3 else None complemento=f"Bloco {random.randint(1, 10)}, Apto {random.randint(1, 999)}" if random.random() < 0.3 else None
) )
session.add(endereco) db.add(endereco)
session.flush() db.flush()
# Selecionar setor e célula aleatórios # Selecionar setor e célula aleatórios
setor = random.choice(setores) setor = random.choice(setores)
celula = random.choice(session.query(Celula).filter_by(setor_id=setor.id).all()) celula = random.choice(db.query(Celula).filter_by(setor_id=setor.id).all())
# Definir responsabilidades # Definir responsabilidades
responsabilidades = 0 responsabilidades = 0
@@ -168,27 +160,34 @@ def criar_militantes(session, num_militantes, setores):
responsabilidades=responsabilidades, responsabilidades=responsabilidades,
estado=random.choice(list(EstadoMilitante)) estado=random.choice(list(EstadoMilitante))
) )
session.add(militante) db.add(militante)
session.flush() db.flush()
# Email único
while True:
email = fake.email()
if email not in emails_usados:
emails_usados.add(email)
break
# Criar email do militante # Criar email do militante
email_militante = EmailMilitante( email_militante = EmailMilitante(
militante_id=militante.id, militante_id=militante.id,
endereco_email=email endereco_email=email
) )
session.add(email_militante) db.add(email_militante)
militantes.append(militante) militantes.append(militante)
session.commit() db.commit()
except Exception as e: except Exception as e:
print(f"Erro ao criar militante {i+1}: {e}") print(f"Erro ao criar militante {i+1}: {e}")
session.rollback() db.rollback()
continue continue
return militantes return militantes
def criar_cotas(session, militantes): def criar_cotas(db, militantes):
"""Cria cotas mensais para os militantes""" """Cria cotas mensais para os militantes"""
print("\nCriando cotas mensais...") print("\nCriando cotas mensais...")
for militante in militantes: for militante in militantes:
@@ -205,16 +204,16 @@ def criar_cotas(session, militantes):
data_vencimento=data_base + timedelta(days=30), data_vencimento=data_base + timedelta(days=30),
pago=random.choice([True, False]) pago=random.choice([True, False])
) )
session.add(cota) db.add(cota)
session.commit() db.commit()
except Exception as e: except Exception as e:
print(f"Erro ao criar cotas para militante {militante.nome}: {e}") print(f"Erro ao criar cotas para militante {militante.nome}: {e}")
session.rollback() db.rollback()
def criar_pagamentos(session, militantes): def criar_pagamentos(db, militantes):
"""Cria pagamentos para os militantes""" """Cria pagamentos para os militantes"""
print("\nCriando pagamentos...") print("\nCriando pagamentos...")
tipos_pagamento = session.query(TipoPagamento).all() tipos_pagamento = db.query(TipoPagamento).all()
for militante in militantes: for militante in militantes:
try: try:
@@ -227,16 +226,16 @@ def criar_pagamentos(session, militantes):
valor=random.uniform(50, 500), valor=random.uniform(50, 500),
data_pagamento=fake.date_between(start_date='-1y', end_date='today') data_pagamento=fake.date_between(start_date='-1y', end_date='today')
) )
session.add(pagamento) db.add(pagamento)
session.commit() db.commit()
except Exception as e: except Exception as e:
print(f"Erro ao criar pagamentos para militante {militante.nome}: {e}") print(f"Erro ao criar pagamentos para militante {militante.nome}: {e}")
session.rollback() db.rollback()
def criar_materiais_vendidos(session, militantes): def criar_materiais_vendidos(db, militantes):
"""Cria registros de materiais vendidos""" """Cria registros de materiais vendidos"""
print("\nCriando materiais vendidos...") print("\nCriando materiais vendidos...")
tipos_material = session.query(TipoMaterial).all() tipos_material = db.query(TipoMaterial).all()
for militante in militantes: for militante in militantes:
try: try:
@@ -249,13 +248,13 @@ def criar_materiais_vendidos(session, militantes):
valor=random.uniform(20, 100), valor=random.uniform(20, 100),
data_venda=fake.date_time_between(start_date='-1y', end_date='now') data_venda=fake.date_time_between(start_date='-1y', end_date='now')
) )
session.add(material) db.add(material)
session.commit() db.commit()
except Exception as e: except Exception as e:
print(f"Erro ao criar materiais vendidos para militante {militante.nome}: {e}") print(f"Erro ao criar materiais vendidos para militante {militante.nome}: {e}")
session.rollback() db.rollback()
def criar_vendas_jornal(session, militantes): def criar_vendas_jornal(db, militantes):
"""Cria vendas de jornal avulso""" """Cria vendas de jornal avulso"""
print("\nCriando vendas de jornal...") print("\nCriando vendas de jornal...")
for militante in militantes: for militante in militantes:
@@ -270,16 +269,16 @@ def criar_vendas_jornal(session, militantes):
valor_total=quantidade * valor_unitario, valor_total=quantidade * valor_unitario,
data_venda=fake.date_time_between(start_date='-1y', end_date='now') data_venda=fake.date_time_between(start_date='-1y', end_date='now')
) )
session.add(venda) db.add(venda)
session.commit() db.commit()
except Exception as e: except Exception as e:
print(f"Erro ao criar vendas de jornal para militante {militante.nome}: {e}") print(f"Erro ao criar vendas de jornal para militante {militante.nome}: {e}")
session.rollback() db.rollback()
def criar_assinaturas(session, militantes): def criar_assinaturas(db, militantes):
"""Cria assinaturas anuais""" """Cria assinaturas anuais"""
print("\nCriando assinaturas anuais...") print("\nCriando assinaturas anuais...")
tipos_material = session.query(TipoMaterial).all() tipos_material = db.query(TipoMaterial).all()
for militante in militantes: for militante in militantes:
try: try:
@@ -294,42 +293,45 @@ def criar_assinaturas(session, militantes):
data_inicio=data_inicio, data_inicio=data_inicio,
data_fim=data_inicio + timedelta(days=365) data_fim=data_inicio + timedelta(days=365)
) )
session.add(assinatura) db.add(assinatura)
session.commit() db.commit()
except Exception as e: except Exception as e:
print(f"Erro ao criar assinatura para militante {militante.nome}: {e}") print(f"Erro ao criar assinatura para militante {militante.nome}: {e}")
session.rollback() db.rollback()
def seed_database(): def seed_database():
"""Função principal para popular o banco de dados""" """Função principal para popular o banco de dados"""
session = get_db_connection() db = get_db_session()
try: try:
print("Iniciando população do banco de dados...") print("Iniciando população do banco de dados...")
# Criar estrutura organizacional
crs, setores = criar_estrutura_organizacional(session)
# Criar tipos básicos # Criar tipos básicos
criar_tipos_pagamento(session) criar_tipos_pagamento(db)
criar_tipos_material(session) criar_tipos_material(db)
# Criar estrutura organizacional
crs, setores = criar_estrutura_organizacional(db)
# Criar militantes (30 militantes para teste) # Criar militantes (30 militantes para teste)
militantes = criar_militantes(session, 30, setores) militantes = criar_militantes(db, 30, setores)
# Criar dados financeiros e materiais # Criar dados financeiros e materiais
criar_cotas(session, militantes) criar_cotas(db, militantes)
criar_pagamentos(session, militantes) criar_pagamentos(db, militantes)
criar_materiais_vendidos(session, militantes) criar_materiais_vendidos(db, militantes)
criar_vendas_jornal(session, militantes) criar_vendas_jornal(db, militantes)
criar_assinaturas(session, militantes) criar_assinaturas(db, militantes)
print("\nBanco de dados populado com sucesso!") print("\nBanco de dados populado com sucesso!")
except Exception as e: except Exception as e:
print(f"Erro durante a população do banco: {e}") print(f"Erro durante a população do banco: {e}")
session.rollback() db.rollback()
finally: finally:
session.close() db.close()
if __name__ == "__main__": if __name__ == "__main__":
seed_database() seed_database()

View File

@@ -1,11 +1,8 @@
from functions.database import get_db_connection, Usuario from functions.database import get_db_session, Usuario
from flask_login import login_user, logout_user from flask_login import login_user, logout_user
from datetime import datetime from datetime import datetime
from typing import Dict, Optional from typing import Dict, Optional
import pyotp from services.otp_service import generate_qr_code_base64
import qrcode
import base64
from io import BytesIO
class AuthService: class AuthService:
"""Service para operações de autenticação""" """Service para operações de autenticação"""
@@ -13,7 +10,7 @@ class AuthService:
@staticmethod @staticmethod
def autenticar_usuario(email_or_username: str, password: str, otp: str = None) -> Dict: def autenticar_usuario(email_or_username: str, password: str, otp: str = None) -> Dict:
"""Autentica um usuário""" """Autentica um usuário"""
db = get_db_connection() db = get_db_session()
try: try:
# Tenta encontrar o usuário por email ou username # Tenta encontrar o usuário por email ou username
user = db.query(Usuario).filter( user = db.query(Usuario).filter(
@@ -64,7 +61,7 @@ class AuthService:
@staticmethod @staticmethod
def desautenticar_usuario(user) -> Dict: def desautenticar_usuario(user) -> Dict:
"""Desautentica um usuário""" """Desautentica um usuário"""
db = get_db_connection() db = get_db_session()
try: try:
if user: if user:
user.logout() user.logout()
@@ -87,7 +84,7 @@ class AuthService:
@staticmethod @staticmethod
def alterar_senha(user_id: int, senha_atual: str, nova_senha: str) -> Dict: def alterar_senha(user_id: int, senha_atual: str, nova_senha: str) -> Dict:
"""Altera a senha de um usuário""" """Altera a senha de um usuário"""
db = get_db_connection() db = get_db_session()
try: try:
user = db.query(Usuario).get(user_id) user = db.query(Usuario).get(user_id)
if not user: if not user:
@@ -122,20 +119,7 @@ class AuthService:
@staticmethod @staticmethod
def gerar_qr_code(user) -> str: def gerar_qr_code(user) -> str:
"""Gera um QR code para o usuário""" """Gera um QR code para o usuário"""
if not user.otp_secret: return generate_qr_code_base64(user)
user.otp_secret = pyotp.random_base32()
totp = pyotp.TOTP(user.otp_secret)
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(totp.provisioning_uri(user.email, issuer_name="Sistema de Controles"))
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buffer = BytesIO()
img.save(buffer, format="PNG")
qr_code = base64.b64encode(buffer.getvalue()).decode('utf-8')
return qr_code
@staticmethod @staticmethod
def verificar_sessao(user) -> Dict: def verificar_sessao(user) -> Dict:

View File

@@ -1,4 +1,4 @@
from functions.database import get_db_connection, Militante, Pagamento, CotaMensal, MaterialVendido, AssinaturaAnual, TipoPagamento from functions.database import get_db_session, Militante, Pagamento, CotaMensal, MaterialVendido, AssinaturaAnual, TipoPagamento
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from datetime import datetime, timedelta from datetime import datetime, timedelta
@@ -15,7 +15,7 @@ class DashboardService:
@cached(expire=300, key_prefix="dashboard") # Cache for 5 minutes @cached(expire=300, key_prefix="dashboard") # Cache for 5 minutes
def get_dashboard_stats() -> Dict[str, Any]: def get_dashboard_stats() -> Dict[str, Any]:
"""Get dashboard statistics with caching""" """Get dashboard statistics with caching"""
db = get_db_connection() db = get_db_session()
try: try:
# Get cached stats first # Get cached stats first
cache_key = CacheKeys.DASHBOARD_STATS cache_key = CacheKeys.DASHBOARD_STATS
@@ -146,7 +146,7 @@ class DashboardService:
@cached(expire=600, key_prefix="dashboard") # Cache for 10 minutes @cached(expire=600, key_prefix="dashboard") # Cache for 10 minutes
def get_militante_stats() -> Dict[str, Any]: def get_militante_stats() -> Dict[str, Any]:
"""Get militante-specific statistics""" """Get militante-specific statistics"""
db = get_db_connection() db = get_db_session()
try: try:
# Militantes por estado # Militantes por estado
estados = db.query(Militante.estado, func.count(Militante.id)).group_by(Militante.estado).all() estados = db.query(Militante.estado, func.count(Militante.id)).group_by(Militante.estado).all()
@@ -175,7 +175,7 @@ class DashboardService:
@cached(expire=300, key_prefix="dashboard") @cached(expire=300, key_prefix="dashboard")
def get_financial_stats() -> Dict[str, Any]: def get_financial_stats() -> Dict[str, Any]:
"""Get financial statistics""" """Get financial statistics"""
db = get_db_connection() db = get_db_session()
try: try:
# Total de pagamentos # Total de pagamentos
total_pagamentos = db.query(func.sum(Pagamento.valor)).scalar() total_pagamentos = db.query(func.sum(Pagamento.valor)).scalar()
@@ -218,7 +218,7 @@ class DashboardService:
@staticmethod @staticmethod
def obter_ultimos_militantes(limite: int = 5) -> List[Militante]: def obter_ultimos_militantes(limite: int = 5) -> List[Militante]:
"""Obtém os últimos militantes cadastrados""" """Obtém os últimos militantes cadastrados"""
db = get_db_connection() db = get_db_session()
try: try:
return db.query(Militante).order_by(Militante.id.desc()).limit(limite).all() return db.query(Militante).order_by(Militante.id.desc()).limit(limite).all()
finally: finally:
@@ -227,7 +227,7 @@ class DashboardService:
@staticmethod @staticmethod
def obter_ultimos_pagamentos(limite: int = 5) -> List[Pagamento]: def obter_ultimos_pagamentos(limite: int = 5) -> List[Pagamento]:
"""Obtém os últimos pagamentos realizados""" """Obtém os últimos pagamentos realizados"""
db = get_db_connection() db = get_db_session()
try: try:
return db.query(Pagamento).join(Militante).order_by(Pagamento.data_pagamento.desc()).limit(limite).all() return db.query(Pagamento).join(Militante).order_by(Pagamento.data_pagamento.desc()).limit(limite).all()
finally: finally:
@@ -236,7 +236,7 @@ class DashboardService:
@staticmethod @staticmethod
def obter_tipos_pagamento() -> List[TipoPagamento]: def obter_tipos_pagamento() -> List[TipoPagamento]:
"""Obtém todos os tipos de pagamento""" """Obtém todos os tipos de pagamento"""
db = get_db_connection() db = get_db_session()
try: try:
return db.query(TipoPagamento).all() return db.query(TipoPagamento).all()
finally: finally:

19
services/otp_service.py Normal file
View File

@@ -0,0 +1,19 @@
import base64
import pyotp
import qrcode
from io import BytesIO
def generate_qr_code(user):
"""Gera imagem PIL do QR code OTP para o usuário."""
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(user.get_otp_uri())
qr.make(fit=True)
return qr.make_image(fill_color="black", back_color="white")
def generate_qr_code_base64(user):
"""Gera QR code OTP codificado em base64 (PNG)."""
img = generate_qr_code(user)
buffer = BytesIO()
img.save(buffer, format="PNG")
return base64.b64encode(buffer.getvalue()).decode("utf-8")

View File

@@ -2,10 +2,15 @@ import os
import sqlite3 import sqlite3
import sys import sys
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv
# Adiciona o diretório raiz ao PYTHONPATH # Adiciona o diretório raiz ao PYTHONPATH
root_dir = str(Path(__file__).parent.parent) ROOT_DIR = Path(__file__).resolve().parents[1]
sys.path.append(root_dir) if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
# Carregar .env antes de importar módulos
load_dotenv(ROOT_DIR / ".env")
from functions.base import Base, engine from functions.base import Base, engine
from functions.database import init_database from functions.database import init_database

View File

@@ -1,25 +1,25 @@
from functions.database import get_db_connection, Usuario from functions.database import get_db_session, Usuario
from functions.rbac import Role, Permission from functions.rbac import Role, Permission
def migrate_existing_users(): def migrate_existing_users():
"""Migra os usuários existentes para o novo sistema RBAC""" """Migra os usuários existentes para o novo sistema RBAC"""
session = get_db_connection() db = get_db_session()
try: try:
# Buscar todos os usuários # Buscar todos os usuários
usuarios = session.query(Usuario).all() usuarios = db.query(Usuario).all()
# Buscar ou criar role de administrador # Buscar ou criar role de administrador
admin_role = session.query(Role).filter_by(nome="Administrador").first() admin_role = db.query(Role).filter_by(nome="Administrador").first()
if not admin_role: if not admin_role:
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL) admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
session.add(admin_role) db.add(admin_role)
# Buscar ou criar role de militante básico # Buscar ou criar role de militante básico
militante_role = session.query(Role).filter_by(nome="Militante Básico").first() militante_role = db.query(Role).filter_by(nome="Militante Básico").first()
if not militante_role: if not militante_role:
militante_role = Role(nome="Militante Básico", nivel=Role.MILITANTE_BASICO) militante_role = Role(nome="Militante Básico", nivel=Role.MILITANTE_BASICO)
session.add(militante_role) db.add(militante_role)
# Atualizar usuários # Atualizar usuários
for usuario in usuarios: for usuario in usuarios:
@@ -33,15 +33,15 @@ def migrate_existing_users():
else: else:
usuario.roles.append(militante_role) usuario.roles.append(militante_role)
session.commit() db.commit()
print("Migração de usuários concluída com sucesso!") print("Migração de usuários concluída com sucesso!")
except Exception as e: except Exception as e:
session.rollback() db.rollback()
print(f"Erro durante a migração de usuários: {str(e)}") print(f"Erro durante a migração de usuários: {str(e)}")
raise e raise e
finally: finally:
session.close() db.close()
if __name__ == '__main__': if __name__ == '__main__':
migrate_existing_users() migrate_existing_users()

View File

@@ -1,6 +1,6 @@
import pytest import pytest
from app import create_app from app import create_app
from functions.database import init_database, get_db_connection from functions.database import init_database, get_db_session
@pytest.fixture @pytest.fixture
def app(): def app():
@@ -15,7 +15,7 @@ def app():
yield app yield app
# Limpar banco após os testes # Limpar banco após os testes
db = get_db_connection() db = get_db_session()
try: try:
db.execute('DROP TABLE IF EXISTS usuarios CASCADE') db.execute('DROP TABLE IF EXISTS usuarios CASCADE')
db.commit() db.commit()

View File

@@ -1,13 +1,13 @@
import pytest import pytest
from flask import url_for from flask import url_for
from functions.database import Usuario, get_db_connection from functions.database import Usuario, get_db_session
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
import json import json
@pytest.fixture @pytest.fixture
def admin_user(client): def admin_user(client):
"""Fixture que cria um usuário admin para testes""" """Fixture que cria um usuário admin para testes"""
db = get_db_connection() db = get_db_session()
try: try:
admin = Usuario( admin = Usuario(
username='admin_test', username='admin_test',
@@ -74,7 +74,7 @@ def test_toggle_status(auth_admin_client, admin_user):
def test_acesso_nao_admin(client): def test_acesso_nao_admin(client):
"""Testa acesso de usuário não admin""" """Testa acesso de usuário não admin"""
db = get_db_connection() db = get_db_session()
try: try:
# Criar usuário normal # Criar usuário normal
user = Usuario( user = Usuario(

View File

@@ -1,7 +1,7 @@
import pytest import pytest
from flask import url_for from flask import url_for
from flask_login import login_user from flask_login import login_user
from functions.database import get_db_connection, Usuario from functions.database import get_db_session, Usuario
import pyotp import pyotp
class TestMenuNavigation: class TestMenuNavigation: