From 449a203926a4b168bab4bfb936726dbd7c7efbc3 Mon Sep 17 00:00:00 2001 From: LS Date: Tue, 1 Apr 2025 15:27:16 -0300 Subject: [PATCH] feat rbac com adicionados os novos campos para hierarquia --- app.py | 74 ++++++++++++++++++++--- functions/database.py | 107 ++++++++++++++++++++++++++++++++++ templates/novo_militante.html | 38 ++++++++++++ 3 files changed, 212 insertions(+), 7 deletions(-) diff --git a/app.py b/app.py index 29b8924..d6e4743 100644 --- a/app.py +++ b/app.py @@ -14,6 +14,7 @@ from functions.database import ( RelatorioVendasMateriais, Usuario, get_db_connection, + ComiteRegional, ) from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker @@ -25,6 +26,7 @@ from functools import wraps from pathlib import Path from time import time from create_admin import generate_qr_code +from flask_mail import Mail, Message app = Flask(__name__) app.secret_key = 'sua_chave_secreta_aqui' # Necessário para sessões do Flask @@ -33,6 +35,15 @@ bootstrap = Bootstrap5(app) # Configuração da sessão do SQLAlchemy db_session = get_db_connection() +# Configurar Flask-Mail +app.config['MAIL_SERVER'] = 'smtp.gmail.com' +app.config['MAIL_PORT'] = 587 +app.config['MAIL_USE_TLS'] = True +app.config['MAIL_USERNAME'] = 'seu-email@gmail.com' +app.config['MAIL_PASSWORD'] = 'sua-senha' + +mail = Mail(app) + # Decorator para verificar se o usuário está logado def login_required(f): @wraps(f) @@ -181,36 +192,74 @@ def home(): @login_required @session_timeout def novo_militante(): + # Obter células disponíveis baseado no nível do usuário + user = db_session.query(Usuario).get(session['user_id']) + celulas_disponiveis = [] + + if user.role.nivel == 1: # CC + celulas_por_cr = db_session.query(ComiteRegional).all() + elif user.role.nivel == 2: # CR + celulas_por_cr = [user.cr] + elif user.role.nivel == 3: # Setor + celulas_por_cr = [{ + 'nome': user.setor.cr.nome, + 'setores': [user.setor] + }] + else: # Célula + celulas_por_cr = [{ + 'nome': user.celula.setor.cr.nome, + 'setores': [{ + 'nome': user.celula.setor.nome, + 'celulas': [user.celula] + }] + }] + if request.method == "POST": cpf = request.form["cpf"] if not validar_cpf(cpf): flash('CPF inválido. Por favor, verifique o número informado.', 'error') return render_template("novo_militante.html", - dados_anteriores=request.form) + dados_anteriores=request.form, + responsabilidades=Militante.get_responsabilidades_list(), + celulas_por_cr=celulas_por_cr) + # Criar militante novo_militante = Militante( nome=request.form["nome"], cpf=cpf, email=request.form["email"], telefone=request.form["telefone"], endereco=request.form["endereco"], - filiado=bool(request.form.get("filiado", False)) + filiado=bool(request.form.get("filiado", False)), + celula_id=request.form["celula_id"] ) - + + # Definir responsabilidades + responsabilidades = [ + int(r) for r in request.form.getlist("responsabilidades") + ] + novo_militante.set_responsabilidades(responsabilidades) + db_session.add(novo_militante) try: db_session.commit() - flash('Militante cadastrado com sucesso!', 'success') + # Enviar email com QR code + novo_militante.send_otp_email(mail) + flash('Militante cadastrado com sucesso! Um email foi enviado com as instruções de autenticação.', 'success') return redirect(url_for("listar_militantes")) except Exception as e: print(e) db_session.rollback() - flash('Erro ao cadastrar militante. Verifique se o CPF ou email já não estão cadastrados.', 'error') + flash('Erro ao cadastrar militante.', 'error') return render_template("novo_militante.html", - dados_anteriores=request.form) + dados_anteriores=request.form, + responsabilidades=Militante.get_responsabilidades_list(), + celulas_por_cr=celulas_por_cr) - return render_template("novo_militante.html") + return render_template("novo_militante.html", + responsabilidades=Militante.get_responsabilidades_list(), + celulas_por_cr=celulas_por_cr) # Rota para listar militantes @app.route("/militantes") @@ -567,6 +616,17 @@ def check_session(): return jsonify({'expired': False}) +@app.route("/qr/") +def get_qr_code(token): + militante = db_session.query(Militante).filter_by(temp_token=token).first() + + if not militante or militante.temp_token_expiry < datetime.now(): + flash('Link expirado ou inválido', 'error') + return redirect(url_for('login')) + + qr_path = generate_qr_code(militante) + return render_template('mostrar_qr_code.html', qr_uri=militante.get_otp_uri()) + def create_app(): app = Flask(__name__) # ... existing code ... diff --git a/functions/database.py b/functions/database.py index d3b16d4..1719042 100644 --- a/functions/database.py +++ b/functions/database.py @@ -6,6 +6,10 @@ import pyotp import os from pathlib import Path from sqlalchemy.pool import NullPool +from datetime import datetime, timedelta +import secrets +from flask_mail import Message +from flask import url_for # Configurar caminho do banco de dados db_dir = Path.home() / '.local' / 'share' / 'controles' @@ -46,6 +50,27 @@ def execute_query(query, params=None): finally: session.close() +class Celula(Base): + __tablename__ = 'celulas' + + id = Column(Integer, primary_key=True, autoincrement=True) + nome = Column(String(100), nullable=False) + setor_id = Column(Integer, ForeignKey('setores.id')) + cr_id = Column(Integer, ForeignKey('comites_regionais.id')) + + setor = relationship("Setor", back_populates="celulas") + cr = relationship("ComiteRegional", back_populates="celulas") + militantes = relationship("Militante", back_populates="celula") + +class ComiteRegional(Base): + __tablename__ = 'comites_regionais' + + id = Column(Integer, primary_key=True, autoincrement=True) + nome = Column(String(100), nullable=False) + + setores = relationship("Setor", back_populates="cr") + celulas = relationship("Celula", back_populates="cr") + class Militante(Base): __tablename__ = 'militantes' @@ -56,12 +81,93 @@ class Militante(Base): telefone = Column(String(15)) endereco = Column(String(255)) filiado = Column(Boolean, default=False) + celula_id = Column(Integer, ForeignKey('celulas.id')) + responsabilidades = Column(Integer, default=0) # Armazenará as responsabilidades como bits + otp_secret = Column(String(32)) + temp_token = Column(String(64)) + temp_token_expiry = Column(DateTime) cotas_mensais = relationship("CotaMensal", back_populates="militante") pagamentos = relationship("Pagamento", back_populates="militante") materiais_vendidos = relationship("MaterialVendido", back_populates="militante") vendas_jornais = relationship("VendaJornalAvulso", back_populates="militante") assinaturas = relationship("AssinaturaAnual", back_populates="militante") + celula = relationship("Celula", back_populates="militantes") + + # Constantes para responsabilidades + SECRETARIO = 1 + TESOUREIRO = 2 + IMPRENSA = 4 + MNS = 8 + MPS = 16 + JUVENTUDE = 32 + + @staticmethod + def get_responsabilidades_list(): + return [ + (Militante.SECRETARIO, "Secretário"), + (Militante.TESOUREIRO, "Tesoureiro"), + (Militante.IMPRENSA, "Imprensa"), + (Militante.MNS, "MNS"), + (Militante.MPS, "MPS"), + (Militante.JUVENTUDE, "Juventude") + ] + + def set_responsabilidades(self, resp_list): + """ + Define as responsabilidades do militante + resp_list: lista de inteiros representando as responsabilidades + """ + self.responsabilidades = sum(resp_list) + + def get_responsabilidades(self): + """ + Retorna lista de responsabilidades ativas + """ + resp = [] + for valor, nome in self.get_responsabilidades_list(): + if self.responsabilidades & valor: + resp.append(nome) + return resp + + def generate_temp_token(self): + """ + Gera um token temporário para acesso ao QR code + """ + self.temp_token = secrets.token_urlsafe(32) + self.temp_token_expiry = datetime.now() + timedelta(hours=48) + return self.temp_token + + def send_otp_email(self, mail): + """ + Envia email com link para QR code + """ + token = self.generate_temp_token() + qr_url = url_for('get_qr_code', token=token, _external=True) + + msg = Message( + 'Configuração de Autenticação em Duas Etapas', + recipients=[self.email] + ) + msg.body = f""" + Olá {self.nome}, + + Para configurar sua autenticação em duas etapas, acesse o link abaixo: + {qr_url} + + Este link expirará em 48 horas. + + Instruções: + 1. Instale um aplicativo autenticador (Google Authenticator, Microsoft Authenticator) + 2. Acesse o link acima + 3. Escaneie o QR code com o aplicativo + 4. Use o código gerado para fazer login no sistema + + Atenciosamente, + Sistema de Controles + """ + + mail.send(msg) class CotaMensal(Base): __tablename__ = 'cotas_mensais' @@ -150,6 +256,7 @@ class Setor(Base): relatorios_cotas = relationship("RelatorioCotasMensais", back_populates="setor") relatorios_vendas = relationship("RelatorioVendasMateriais", back_populates="setor") usuarios = relationship("Usuario", back_populates="setor") + celulas = relationship("Celula", back_populates="setor") class ComiteCentral(Base): __tablename__ = 'comites_centrais' diff --git a/templates/novo_militante.html b/templates/novo_militante.html index a9d5919..da7d850 100644 --- a/templates/novo_militante.html +++ b/templates/novo_militante.html @@ -55,6 +55,44 @@ +
+ +
+ {% for valor, nome in responsabilidades %} +
+
+ + +
+
+ {% endfor %} +
+
+ + {% if celulas %} +
+ + +
+ {% endif %} +
Voltar