feat rbac com adicionados os novos campos para hierarquia
This commit is contained in:
72
app.py
72
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/<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 ...
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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=" 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>
|
||||
|
||||
Reference in New Issue
Block a user