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