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

74
app.py
View File

@@ -14,6 +14,7 @@ from functions.database import (
RelatorioVendasMateriais, RelatorioVendasMateriais,
Usuario, Usuario,
get_db_connection, get_db_connection,
ComiteRegional,
) )
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
@@ -25,6 +26,7 @@ from functools import wraps
from pathlib import Path from pathlib import Path
from time import time from time import time
from create_admin import generate_qr_code from create_admin import generate_qr_code
from flask_mail import Mail, Message
app = Flask(__name__) app = Flask(__name__)
app.secret_key = 'sua_chave_secreta_aqui' # Necessário para sessões do Flask 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 # Configuração da sessão do SQLAlchemy
db_session = get_db_connection() 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 # Decorator para verificar se o usuário está logado
def login_required(f): def login_required(f):
@wraps(f) @wraps(f)
@@ -181,36 +192,74 @@ def home():
@login_required @login_required
@session_timeout @session_timeout
def novo_militante(): 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": if request.method == "POST":
cpf = request.form["cpf"] cpf = request.form["cpf"]
if not validar_cpf(cpf): if not validar_cpf(cpf):
flash('CPF inválido. Por favor, verifique o número informado.', 'error') flash('CPF inválido. Por favor, verifique o número informado.', 'error')
return render_template("novo_militante.html", 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( novo_militante = Militante(
nome=request.form["nome"], nome=request.form["nome"],
cpf=cpf, cpf=cpf,
email=request.form["email"], email=request.form["email"],
telefone=request.form["telefone"], telefone=request.form["telefone"],
endereco=request.form["endereco"], 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) db_session.add(novo_militante)
try: try:
db_session.commit() 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")) return redirect(url_for("listar_militantes"))
except Exception as e: except Exception as e:
print(e) print(e)
db_session.rollback() 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", 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 # Rota para listar militantes
@app.route("/militantes") @app.route("/militantes")
@@ -567,6 +616,17 @@ def check_session():
return jsonify({'expired': False}) 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(): def create_app():
app = Flask(__name__) app = Flask(__name__)
# ... existing code ... # ... existing code ...

View File

@@ -6,6 +6,10 @@ import pyotp
import os import os
from pathlib import Path from pathlib import Path
from sqlalchemy.pool import NullPool 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 # Configurar caminho do banco de dados
db_dir = Path.home() / '.local' / 'share' / 'controles' db_dir = Path.home() / '.local' / 'share' / 'controles'
@@ -46,6 +50,27 @@ def execute_query(query, params=None):
finally: finally:
session.close() 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): class Militante(Base):
__tablename__ = 'militantes' __tablename__ = 'militantes'
@@ -56,12 +81,93 @@ class Militante(Base):
telefone = Column(String(15)) telefone = Column(String(15))
endereco = Column(String(255)) endereco = Column(String(255))
filiado = Column(Boolean, default=False) 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") cotas_mensais = relationship("CotaMensal", back_populates="militante")
pagamentos = relationship("Pagamento", back_populates="militante") pagamentos = relationship("Pagamento", back_populates="militante")
materiais_vendidos = relationship("MaterialVendido", back_populates="militante") materiais_vendidos = relationship("MaterialVendido", back_populates="militante")
vendas_jornais = relationship("VendaJornalAvulso", back_populates="militante") vendas_jornais = relationship("VendaJornalAvulso", back_populates="militante")
assinaturas = relationship("AssinaturaAnual", 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): class CotaMensal(Base):
__tablename__ = 'cotas_mensais' __tablename__ = 'cotas_mensais'
@@ -150,6 +256,7 @@ class Setor(Base):
relatorios_cotas = relationship("RelatorioCotasMensais", back_populates="setor") relatorios_cotas = relationship("RelatorioCotasMensais", back_populates="setor")
relatorios_vendas = relationship("RelatorioVendasMateriais", back_populates="setor") relatorios_vendas = relationship("RelatorioVendasMateriais", back_populates="setor")
usuarios = relationship("Usuario", back_populates="setor") usuarios = relationship("Usuario", back_populates="setor")
celulas = relationship("Celula", back_populates="setor")
class ComiteCentral(Base): class ComiteCentral(Base):
__tablename__ = 'comites_centrais' __tablename__ = 'comites_centrais'

View File

@@ -55,6 +55,44 @@
<label class="form-check-label" for="filiado">Filiado</label> <label class="form-check-label" for="filiado">Filiado</label>
</div> </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"> <div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Criar</button> <button type="submit" class="btn btn-primary">Criar</button>
<a href="{{ url_for('listar_militantes') }}" class="btn btn-secondary">Voltar</a> <a href="{{ url_for('listar_militantes') }}" class="btn btn-secondary">Voltar</a>