feat rbac com adicionados os novos campos para hierarquia

This commit is contained in:
LS
2025-04-01 15:27:16 -03:00
parent 01f5901eb2
commit 449a203926
3 changed files with 212 additions and 7 deletions

72
app.py
View File

@@ -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/<token>")
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 ...

View File

@@ -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'

View File

@@ -55,6 +55,44 @@
<label class="form-check-label" for="filiado">Filiado</label>
</div>
<div class="mb-3">
<label class="form-label">Responsabilidades:</label>
<div class="row">
{% for valor, nome in responsabilidades %}
<div class="col-md-4">
<div class="form-check">
<input type="checkbox" class="form-check-input"
id="resp_{{ valor }}" name="responsabilidades"
value="{{ valor }}">
<label class="form-check-label" for="resp_{{ valor }}">
{{ nome }}
</label>
</div>
</div>
{% endfor %}
</div>
</div>
{% if celulas %}
<div class="mb-3">
<label for="celula" class="form-label">Célula:</label>
<select class="form-select" id="celula" name="celula_id" required>
<option value="">Selecione uma célula</option>
{% for cr in celulas_por_cr %}
<optgroup label="CR {{ cr.nome }}">
{% for setor in cr.setores %}
<optgroup label="&nbsp;&nbsp;Setor {{ setor.nome }}">
{% for celula in setor.celulas %}
<option value="{{ celula.id }}">{{ celula.nome }}</option>
{% endfor %}
</optgroup>
{% endfor %}
</optgroup>
{% endfor %}
</select>
</div>
{% endif %}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Criar</button>
<a href="{{ url_for('listar_militantes') }}" class="btn btn-secondary">Voltar</a>