From cbaf227e58b93d3c1bca4d333c729e8e1ebb442f Mon Sep 17 00:00:00 2001 From: LS Date: Thu, 3 Apr 2025 15:58:07 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20implementa=20sistema=20de=20responsabil?= =?UTF-8?q?idades=20e=20inst=C3=A2ncias=20-=20Adiciona=20responsabilidades?= =?UTF-8?q?=20de=20Finan=C3=A7as=20e=20Imprensa=20para=20todas=20as=20inst?= =?UTF-8?q?=C3=A2ncias=20-=20Cria=20templates=20gen=C3=A9ricos=20para=20ge?= =?UTF-8?q?renciamento=20de=20inst=C3=A2ncias=20-=20Implementa=20sistema?= =?UTF-8?q?=20de=20permiss=C3=B5es=20baseado=20em=20RBAC=20-=20Adiciona=20?= =?UTF-8?q?status=20de=20Aspirante=20com=20avalia=C3=A7=C3=A3o=20obrigat?= =?UTF-8?q?=C3=B3ria=20-=20Atualiza=20documenta=C3=A7=C3=A3o=20com=20novas?= =?UTF-8?q?=20regras=20e=20responsabilidades=20-=20Cria=20testes=20para=20?= =?UTF-8?q?valida=C3=A7=C3=A3o=20das=20permiss=C3=B5es=20-=20Adiciona=20mi?= =?UTF-8?q?gra=C3=A7=C3=A3o=20para=20novos=20campos=20no=20banco=20de=20da?= =?UTF-8?q?dos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 161 ++- admin_qr.txt | 1 + app.py | 1172 ++++++++++++----- config.py | 1 - create_admin.py | 183 +-- create_test_users.py | 104 ++ dao.py | 8 - database.sql | 99 -- docs/README.md | 165 +++ docs/rbac.md | 239 ++++ functions/base.py | 21 + functions/database.py | 340 +++-- functions/decorators.py | 197 +++ functions/permissions.py | 222 ++++ functions/rbac.py | 292 ++++ init_system.py | 58 + .../add_responsaveis_financas_imprensa.py | 64 + requirements.txt | 16 +- setup.py | 18 + sql/migrate_db.py | 66 + sql/migrate_rbac.py | 47 + sql/rbac_tables.sql | 152 +++ templates/alterar_senha.html | 51 + templates/base.html | 103 +- templates/criar_instancia.html | 111 ++ templates/criar_militante.html | 106 ++ templates/dashboard.html | 284 ++++ templates/dashboard_admin.html | 83 ++ templates/editar_instancia.html | 111 ++ templates/editar_militante.html | 106 +- templates/home.html | 74 +- templates/listar_instancias.html | 79 ++ templates/listar_militantes.html | 83 +- templates/login.html | 33 +- templates/novo_militante.html | 129 +- templates/novo_usuario.html | 74 -- tests/test_permissions.py | 205 +++ 37 files changed, 4305 insertions(+), 953 deletions(-) create mode 100644 admin_qr.txt delete mode 100644 config.py create mode 100644 create_test_users.py delete mode 100644 dao.py delete mode 100644 database.sql create mode 100644 docs/README.md create mode 100644 docs/rbac.md create mode 100644 functions/base.py create mode 100644 functions/decorators.py create mode 100644 functions/permissions.py create mode 100644 functions/rbac.py create mode 100644 init_system.py create mode 100644 migrations/versions/add_responsaveis_financas_imprensa.py create mode 100644 setup.py create mode 100644 sql/migrate_db.py create mode 100644 sql/migrate_rbac.py create mode 100644 sql/rbac_tables.sql create mode 100644 templates/alterar_senha.html create mode 100644 templates/criar_instancia.html create mode 100644 templates/criar_militante.html create mode 100644 templates/dashboard.html create mode 100644 templates/dashboard_admin.html create mode 100644 templates/editar_instancia.html create mode 100644 templates/listar_instancias.html delete mode 100644 templates/novo_usuario.html create mode 100644 tests/test_permissions.py diff --git a/README.md b/README.md index c4a2e1c..11713c4 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,134 @@ -# Sistema de Controles +# Sistema de Controle de Militantes -## Para instalar +Sistema para gerenciamento de militantes, células, setores e comitês regionais. -```bash -make install +## Estrutura de Permissões (RBAC) + +O sistema utiliza um sistema de controle de acesso baseado em papéis (RBAC) com a seguinte hierarquia: + +### Níveis de Papéis + +1. **Militante Básico** (Nível 1) + - Visualizar próprios dados + - Editar próprios dados + - Visualizar dados da célula + +2. **Secretário de Célula** (Nível 2) + - Todas as permissões do Militante Básico + - Gerenciar membros da célula + - Criar membros na célula + - Visualizar relatórios da célula + +3. **Membro de Setor** (Nível 3) + - Todas as permissões do Secretário de Célula + - Visualizar relatórios do setor + +4. **Secretário de Setor** (Nível 4) + - Todas as permissões do Membro de Setor + - Gerenciar células do setor + - Criar células no setor + +5. **Membro de CR** (Nível 5) + - Todas as permissões do Secretário de Setor + - Visualizar relatórios do CR + +6. **Secretário de CR** (Nível 6) + - Todas as permissões do Membro de CR + - Gerenciar setores do CR + - Criar setores no CR + +7. **Membro do CC** (Nível 7) + - Todas as permissões do Secretário de CR + - Visualizar relatórios nacionais + +8. **Secretário Geral** (Nível 8) + - Todas as permissões do Membro do CC + - Gerenciar CRs + - Criar CRs + - Configurar sistema + +## Instalação + +1. Clone o repositório +2. Crie um ambiente virtual: + ```bash + python -m venv venv + source venv/bin/activate # Linux/Mac + # ou + venv\Scripts\activate # Windows + ``` +3. Instale as dependências: + ```bash + pip install -r requirements.txt + ``` +4. Execute as migrações do banco de dados: + ```bash + python sql/migrate_db.py + ``` +5. Configure as variáveis de ambiente no arquivo `.env`: + ``` + FLASK_APP=app.py + FLASK_ENV=development + SECRET_KEY=sua_chave_secreta + MAIL_SERVER=seu_servidor_smtp + MAIL_PORT=587 + MAIL_USE_TLS=True + MAIL_USERNAME=seu_email + MAIL_PASSWORD=sua_senha + ``` +6. Execute o aplicativo: + ```bash + flask run + ``` + +## Uso + +### Decoradores de Permissão + +O sistema fornece três decoradores para controle de acesso: + +1. `@require_permission(permission_name)` + - Verifica se o usuário tem uma permissão específica + - Exemplo: `@require_permission('create_cell_member')` + +2. `@require_role(role_name)` + - Verifica se o usuário tem um papel específico + - Exemplo: `@require_role('Secretário de Célula')` + +3. `@require_minimum_role(min_level)` + - Verifica se o usuário tem um papel com nível mínimo + - Exemplo: `@require_minimum_role(Role.SECRETARIO_CR)` + +### Verificando Permissões no Código + +```python +# Verificar se um usuário tem uma permissão +if user.has_permission('create_cell_member'): + # Faça algo + +# Verificar se um usuário tem um papel +if user.has_role('Secretário de Célula'): + # Faça algo + +# Obter o papel mais alto do usuário +highest_role = user.get_highest_role() +if highest_role and highest_role.nivel >= Role.SECRETARIO_CR: + # Faça algo ``` -## Sobre o QR Code de Autenticação (admin_qr.png) +## Estrutura do Banco de Dados -O arquivo `admin_qr.png` é um QR code gerado automaticamente para configuração da autenticação de dois fatores (2FA) do usuário administrador. Este arquivo é: +O sistema utiliza as seguintes tabelas para o RBAC: -- Gerado na raiz do projeto quando: - - O comando `make reset-admin` é executado - - O servidor é iniciado com `make run` e existe um usuário admin - - Um novo usuário é criado através da interface web +- `roles`: Armazena os papéis disponíveis +- `permissions`: Armazena as permissões disponíveis +- `role_permissions`: Mapeia papéis para permissões +- `user_roles`: Mapeia usuários para papéis -- Usado para: - - Configurar a autenticação 2FA no aplicativo autenticador (Google Authenticator, Microsoft Authenticator, etc.) - - Gerar os códigos OTP necessários para fazer login no sistema +## Segurança -### Importante: -- O QR code é atualizado sempre que um novo usuário é criado ou quando o sistema é reiniciado -- Cada QR code é único e corresponde ao segredo OTP atual do usuário -- Se você recriar o banco de dados ou resetar o admin, será necessário reconfigurar o aplicativo autenticador com o novo QR code -- Mantenha este arquivo seguro, pois ele contém informações sensíveis de autenticação - -### Como usar: -1. Instale um aplicativo autenticador no seu celular (Google Authenticator, Microsoft Authenticator, etc.) -2. Escaneie o QR code (`admin_qr.png`) com o aplicativo -3. O aplicativo irá gerar códigos de 6 dígitos a cada 30 segundos -4. Use estes códigos junto com seu usuário e senha para fazer login no sistema - -### Comandos relacionados: -```bash -# Limpar banco e criar novo admin com novo QR code -make reset-admin - -# Iniciar o servidor (também gera novo QR code se necessário) -make run -``` - -Acesse por: http://127.0.0.1:5000 +- Todas as senhas são armazenadas com hash bcrypt +- Sessões expiram após período de inatividade +- Controle de acesso granular baseado em papéis +- Proteção contra CSRF +- Validação de entrada de dados diff --git a/admin_qr.txt b/admin_qr.txt new file mode 100644 index 0000000..543118b --- /dev/null +++ b/admin_qr.txt @@ -0,0 +1 @@ +otpauth://totp/Sistema%20de%20Controles:admin?secret=27NESPSPWKWIXVIDBUJPTK7MPAKGF4WG&issuer=Sistema%20de%20Controles \ No newline at end of file diff --git a/app.py b/app.py index ec1e9da..f6d5632 100644 --- a/app.py +++ b/app.py @@ -1,4 +1,4 @@ -from flask import Flask, request, render_template, redirect, url_for, flash, session, jsonify +from flask import Flask, request, render_template, redirect, url_for, flash, session, jsonify, send_file from functions.database import ( Base, Militante, @@ -17,9 +17,12 @@ from functions.database import ( ComiteRegional, Setor, Celula, + ComiteCentral, + EmailMilitante, + init_database, ) from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import sessionmaker, joinedload from datetime import datetime, timedelta from flask_bootstrap import Bootstrap5 from routes.cota import cota_bp @@ -27,17 +30,65 @@ from functions.validations import validar_cpf 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 import os from dotenv import load_dotenv +from functions.rbac import Role, Permission, init_rbac +from functions.decorators import require_permission, require_role, require_minimum_role, require_login, require_instance_permission, require_instance_access +import re +import secrets +import pyotp +import qrcode +import base64 +from io import BytesIO +from create_admin import create_admin +from create_test_users import create_test_users +from werkzeug.security import generate_password_hash, check_password_hash +from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user +import random +import string load_dotenv() app = Flask(__name__) -app.secret_key = 'sua_chave_secreta_aqui' # Necessário para sessões do Flask +app.secret_key = os.getenv('SECRET_KEY', secrets.token_hex(16)) bootstrap = Bootstrap5(app) +# Configurar Flask-Login +login_manager = LoginManager() +login_manager.init_app(app) +login_manager.login_view = 'login' + +@login_manager.user_loader +def load_user(user_id): + """Carrega o usuário pelo ID""" + db = get_db_connection() + try: + # Carregar o usuário com suas roles + user = db.query(Usuario).options( + joinedload(Usuario.roles) + ).get(user_id) + return user + finally: + db.close() + +def generate_qr_code(user): + """Gera um QR code para o usuário""" + if not user.otp_secret: + user.otp_secret = pyotp.random_base32() + + totp = pyotp.TOTP(user.otp_secret) + qr = qrcode.QRCode(version=1, box_size=10, border=5) + qr.add_data(totp.provisioning_uri(user.email, issuer_name="Sistema de Controles")) + qr.make(fit=True) + + img = qr.make_image(fill_color="black", back_color="white") + buffer = BytesIO() + img.save(buffer, format="PNG") + qr_code = base64.b64encode(buffer.getvalue()).decode('utf-8') + + return qr_code + # Registrar blueprints app.register_blueprint(cota_bp) @@ -45,18 +96,27 @@ app.register_blueprint(cota_bp) 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' # Substituir pelo seu email -app.config['MAIL_PASSWORD'] = 'sua-senha-de-app' # Substituir pela senha de app - -# Se estiver usando variáveis de ambiente (recomendado): -app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME') -app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD') +app.config['MAIL_SERVER'] = os.getenv('MAIL_SERVER', 'smtp.gmail.com') +app.config['MAIL_PORT'] = int(os.getenv('MAIL_PORT', 587)) +app.config['MAIL_USE_TLS'] = os.getenv('MAIL_USE_TLS', 'True').lower() == 'true' +app.config['MAIL_USERNAME'] = os.getenv('MAIL_USERNAME') +app.config['MAIL_PASSWORD'] = os.getenv('MAIL_PASSWORD') +app.config['MAIL_DEFAULT_SENDER'] = os.getenv('MAIL_DEFAULT_SENDER') mail = Mail(app) +# Inicializar banco de dados e RBAC +print("Inicializando banco de dados...") +init_database() + +print("Inicializando sistema RBAC...") +init_rbac() + +# Criar admin e usuários de teste +print("Criando usuários iniciais...") +create_admin() +create_test_users() + # Decorator para verificar se o usuário está logado def login_required(f): @wraps(f) @@ -79,8 +139,9 @@ def session_timeout(f): last_activity = datetime.fromtimestamp(session['last_activity']) now = datetime.now() - # Se passaram mais de 2 horas - if now - last_activity > timedelta(hours=2): + # Se passaram mais de 30 minutos (configurável) + timeout_minutes = 30 + if now - last_activity > timedelta(minutes=timeout_minutes): # Registrar o logout por timeout try: user = db_session.query(Usuario).get(session['user_id']) @@ -97,248 +158,212 @@ def session_timeout(f): # Atualizar timestamp de último acesso session['last_activity'] = time() + + # Atualizar também no banco de dados + try: + user = db_session.query(Usuario).get(session['user_id']) + if user: + user.update_last_activity() + db_session.commit() + except Exception as e: + print(f"Erro ao atualizar última atividade: {e}") + return f(*args, **kwargs) return decorated_function # Rota raiz - redireciona para login se não estiver autenticado @app.route("/") +@require_login def index(): - if 'user_id' not in session: - return redirect(url_for('login')) + """Rota principal""" return redirect(url_for('home')) # Rota de login @app.route("/login", methods=["GET", "POST"]) def login(): - if 'user_id' in session: - return redirect(url_for('home')) - + """Rota de login""" if request.method == "POST": username = request.form.get("username") password = request.form.get("password") - otp = request.form.get("otp") + otp_code = request.form.get("otp_code") - # Log dos dados recebidos (sem a senha) - print(f"Tentativa de login - Username: {username}, OTP fornecido: {'Sim' if otp else 'Não'}") + if not all([username, password, otp_code]): + flash("Todos os campos são obrigatórios.", "error") + return redirect(url_for("login")) - user = db_session.query(Usuario).filter_by(username=username).first() - - if not user: - print(f"Erro: Usuário '{username}' não encontrado") - flash('Usuário não encontrado', 'danger') - return render_template('login.html') + db = get_db_connection() + try: + user = db.query(Usuario).filter_by(username=username).first() - if not user.check_password(password): - print(f"Erro: Senha incorreta para o usuário '{username}'") - flash('Senha incorreta', 'danger') - return render_template('login.html') + if not user or not user.check_password(password): + flash("Usuário ou senha incorretos.", "error") + return redirect(url_for("login")) - if not user.verify_otp(otp): - print(f"Erro: Código OTP inválido para o usuário '{username}'") - print(f"OTP fornecido: {otp}") - print(f"OTP secret do usuário: {user.otp_secret}") - flash('Código OTP inválido', 'danger') - return render_template('login.html') - - # Se chegou aqui, login bem sucedido - print(f"Login bem sucedido para o usuário '{username}'") - session['user_id'] = user.id - session['is_admin'] = user.is_admin - session['last_activity'] = time() # Inicializar timestamp - session['username'] = user.username # Armazenar apenas o username - - # Registrar horário do login - user.ultimo_login = datetime.now() - db_session.commit() - - next_page = request.args.get('next') - flash('Login realizado com sucesso!', 'success') - return redirect(next_page or url_for('home')) + if not user.verify_otp(otp_code): + flash("Código OTP inválido.", "error") + return redirect(url_for("login")) + + # Atualizar último login + user.ultimo_login = datetime.utcnow() + db.commit() + + # Fazer login + login_user(user) + + # Redirecionar para home + return redirect(url_for("home")) + finally: + db.close() - return render_template('login.html') + return render_template("login.html") # Rota de logout @app.route("/logout") +@login_required def logout(): - session.clear() # Limpa a sessão do Flask - flash('Você foi desconectado com sucesso.', 'info') + db = get_db_connection() + try: + user = current_user + if user: + user.logout() + db.commit() + logout_user() + finally: + db.close() + flash('Logout realizado com sucesso!', 'success') return redirect(url_for('login')) # Rota home (protegida) @app.route("/home") -@login_required -@session_timeout +@require_login def home(): - """Página inicial do sistema""" - try: - links = [] - # Filtrar apenas as rotas que queremos mostrar - allowed_endpoints = { - 'listar_militantes': 'Militantes', - 'listar_cotas': 'Cotas', - 'listar_pagamentos': 'Pagamentos', - 'listar_materiais': 'Materiais', - 'listar_vendas_jornal': 'Vendas de Jornal', - 'listar_assinaturas': 'Assinaturas' - } - - for rule in app.url_map.iter_rules(): - if (rule.endpoint in allowed_endpoints and - "GET" in rule.methods and - len(rule.arguments) == 0): # Apenas rotas sem parâmetros - url = url_for(rule.endpoint) - nome = allowed_endpoints[rule.endpoint] - links.append((url, nome)) - - # Ordenar links pelo nome - links.sort(key=lambda x: x[1]) - - return render_template('home.html', links=links, current_user={'username': session.get('username')}) - except Exception as e: - print(f"Erro na página inicial: {e}") - import traceback - traceback.print_exc() - flash('Erro ao carregar a página inicial', 'error') - return render_template('home.html', links=[], current_user={'username': session.get('username')}) + """Página inicial""" + links = [] + + # Links básicos para todos os usuários + links.append({ + 'text': 'Alterar Senha', + 'url': url_for('alterar_senha') + }) + + # Links específicos baseados em permissões + if current_user.has_permission('view_cell_data'): + links.append({ + 'text': 'Militantes', + 'url': url_for('listar_militantes') + }) + + if current_user.has_permission('view_cell_reports'): + links.append({ + 'text': 'Pagamentos', + 'url': url_for('listar_pagamentos') + }) + links.append({ + 'text': 'Materiais', + 'url': url_for('listar_materiais') + }) + links.append({ + 'text': 'Vendas', + 'url': url_for('listar_relatorios_vendas') + }) + + # Links para admin + if current_user.is_admin: + links.append({ + 'text': 'Dashboard Admin', + 'url': url_for('dashboard_admin') + }) + + return render_template('home.html', links=links) # Rota para criar um novo militante -@app.route("/militantes/novo", methods=["GET", "POST"]) -@login_required -@session_timeout -def novo_militante(): - user = db_session.query(Usuario).get(session['user_id']) - - try: - # Obter CRs e células existentes - if user.is_admin: - celulas_por_cr = db_session.query(ComiteRegional).all() - crs_existentes = celulas_por_cr - elif user.cr_id: - celulas_por_cr = [user.cr] - crs_existentes = [user.cr] - else: - celulas_por_cr = [] - crs_existentes = [] - - except Exception as e: - print(f"Erro ao obter células: {e}") - celulas_por_cr = [] - crs_existentes = [] - - if request.method == "POST": +@app.route('/militantes/criar', methods=['GET', 'POST']) +@require_login +@require_permission('gerenciar_militantes') +def criar_militante(): + if request.method == 'POST': try: - # Validar CPF - cpf = request.form.get("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, - responsabilidades=Militante.get_responsabilidades_list(), - celulas_por_cr=celulas_por_cr, - crs_existentes=crs_existentes) + nome = request.form['nome'] + email = request.form['email'] + cpf = request.form['cpf'] + titulo_eleitoral = request.form['titulo_eleitoral'] + data_nascimento = datetime.strptime(request.form['data_nascimento'], '%Y-%m-%d').date() if request.form['data_nascimento'] else None + data_entrada_oci = datetime.strptime(request.form['data_entrada_oci'], '%Y-%m-%d').date() if request.form['data_entrada_oci'] else None + data_efetivacao_oci = datetime.strptime(request.form['data_efetivacao_oci'], '%Y-%m-%d').date() if request.form['data_efetivacao_oci'] else None + telefone1 = request.form['telefone1'] + telefone2 = request.form['telefone2'] + profissao = request.form['profissao'] + regime_trabalho = request.form['regime_trabalho'] + empresa = request.form['empresa'] + contratante = request.form['contratante'] + instituicao_ensino = request.form['instituicao_ensino'] + tipo_instituicao = request.form['tipo_instituicao'] + sindicato = request.form['sindicato'] + cargo_sindical = request.form['cargo_sindical'] + dirigente_sindical = 'dirigente_sindical' in request.form + central_sindical = request.form['central_sindical'] + setor_id = request.form['setor_id'] + celula_id = request.form['celula_id'] - celula_id = None - if request.form.get("nova_celula"): - # Criar nova estrutura organizacional se necessário - cr = None - if request.form.get("nome_cr") == "novo": - cr = ComiteRegional(nome=request.form.get("novo_cr")) - db_session.add(cr) - db_session.flush() - else: - cr_id = request.form.get("nome_cr") - cr = db_session.query(ComiteRegional).get(cr_id) - - setor = None - if not request.form.get("sem_setor"): - if request.form.get("nome_setor") == "novo": - setor = Setor(nome=request.form.get("novo_setor"), cr_id=cr.id) - db_session.add(setor) - db_session.flush() - elif request.form.get("nome_setor"): - setor = db_session.query(Setor).get(request.form.get("nome_setor")) - - # Criar nova célula - celula = Celula( - nome=request.form.get("nome_celula"), - cr_id=cr.id, - setor_id=setor.id if setor else None - ) - db_session.add(celula) - db_session.flush() - celula_id = celula.id - else: - celula_id = request.form.get("celula_id") + # Processar responsabilidades + responsabilidades = 0 + if 'responsabilidades' in request.form: + for responsabilidade in request.form.getlist('responsabilidades'): + responsabilidades |= int(responsabilidade) - # Criar militante com todos os campos - novo_militante = Militante( - nome=request.form.get("nome"), + militante = Militante( + nome=nome, + email=email, cpf=cpf, - titulo_eleitoral=request.form.get("titulo_eleitoral"), - data_nascimento=datetime.strptime(request.form.get("data_nascimento"), "%Y-%m-%d") if request.form.get("data_nascimento") else None, - data_entrada_oci=datetime.strptime(request.form.get("data_entrada_oci"), "%Y-%m-%d") if request.form.get("data_entrada_oci") else None, - data_efetivacao_oci=datetime.strptime(request.form.get("data_efetivacao_oci"), "%Y-%m-%d") if request.form.get("data_efetivacao_oci") else None, - telefone1=request.form.get("telefone1"), - telefone2=request.form.get("telefone2"), - profissao=request.form.get("profissao"), - regime_trabalho=request.form.get("regime_trabalho"), - empresa=request.form.get("empresa"), - contratante=request.form.get("contratante"), - instituicao_ensino=request.form.get("instituicao_ensino"), - tipo_instituicao=request.form.get("tipo_instituicao"), - sindicato=request.form.get("sindicato"), - cargo_sindical=request.form.get("cargo_sindical"), - dirigente_sindical=bool(request.form.get("dirigente_sindical")), - central_sindical=request.form.get("central_sindical"), - celula_id=celula_id + titulo_eleitoral=titulo_eleitoral, + data_nascimento=data_nascimento, + data_entrada_oci=data_entrada_oci, + data_efetivacao_oci=data_efetivacao_oci, + telefone1=telefone1, + telefone2=telefone2, + profissao=profissao, + regime_trabalho=regime_trabalho, + empresa=empresa, + contratante=contratante, + instituicao_ensino=instituicao_ensino, + tipo_instituicao=tipo_instituicao, + sindicato=sindicato, + cargo_sindical=cargo_sindical, + dirigente_sindical=dirigente_sindical, + central_sindical=central_sindical, + setor_id=setor_id, + celula_id=celula_id, + responsabilidades=responsabilidades ) - # 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(militante) db_session.commit() - # Tentar enviar email - try: - novo_militante.send_otp_email(mail) - flash('Militante cadastrado com sucesso! Um email foi enviado com as instruções de autenticação.', 'success') - except Exception as mail_error: - print(f"Erro ao enviar email: {mail_error}") - flash('Militante cadastrado com sucesso, mas houve um erro ao enviar o email. Entre em contato com o administrador.', 'warning') - - return redirect(url_for("listar_militantes")) + flash('Militante criado com sucesso!', 'success') + return redirect(url_for('listar_militantes')) except Exception as e: - print(f"Erro ao cadastrar militante: {e}") db_session.rollback() - flash('Erro ao cadastrar militante.', 'error') - return render_template("novo_militante.html", - dados_anteriores=request.form, - responsabilidades=Militante.get_responsabilidades_list(), - celulas_por_cr=celulas_por_cr, - crs_existentes=crs_existentes) - - return render_template("novo_militante.html", - responsabilidades=Militante.get_responsabilidades_list(), - celulas_por_cr=celulas_por_cr, - crs_existentes=crs_existentes) + flash(f'Erro ao criar militante: {str(e)}', 'danger') + return redirect(url_for('criar_militante')) + + setores = Setor.query.all() + celulas = Celula.query.all() + return render_template('criar_militante.html', setores=setores, celulas=celulas) # Rota para listar militantes @app.route("/militantes") -@login_required -@session_timeout +@require_login +@require_permission(Permission.VIEW_CELL_DATA) def listar_militantes(): + """Lista todos os militantes""" militantes = db_session.query(Militante).all() return render_template("listar_militantes.html", militantes=militantes) # Rota para criar uma nova cota mensal @app.route("/cotas/novo", methods=["GET", "POST"]) -@login_required -@session_timeout +@require_login +@require_permission(Permission.VIEW_CELL_REPORTS) def nova_cota(): if request.method == "POST": cotas_mensais = CotaMensal( @@ -362,17 +387,28 @@ def nova_cota(): # Rota para listar cotas mensais @app.route("/cotas") -@login_required -@session_timeout +@require_login +@require_permission(Permission.VIEW_CELL_REPORTS) def listar_cotas(): + """Lista todas as cotas""" cotas = db_session.query(CotaMensal).all() return render_template("listar_cotas.html", cotas=cotas) # Rota para criar um novo pagamento @app.route("/pagamentos/novo", methods=["GET", "POST"]) -@login_required -@session_timeout +@require_login def novo_pagamento(): + # Verificar permissões do usuário + user = db_session.query(Usuario).get(session['user_id']) + + # Verificar se o usuário tem permissão para registrar pagamentos em alguma instância + if not (user.has_permission(Permission.REGISTER_CELL_PAYMENT) or + user.has_permission(Permission.REGISTER_SECTOR_PAYMENT) or + user.has_permission(Permission.REGISTER_CR_PAYMENT) or + user.has_permission(Permission.REGISTER_CC_PAYMENT)): + flash('Você não tem permissão para registrar pagamentos.', 'error') + return redirect(url_for('home')) + if request.method == "POST": pagamentos = Pagamento( militante_id=request.form["militante_id"], @@ -395,16 +431,40 @@ def novo_pagamento(): # Rota para listar pagamentos @app.route("/pagamentos") -@login_required -@session_timeout +@require_login def listar_pagamentos(): - pagamentos = db_session.query(Pagamento).all() + """Lista todos os pagamentos""" + user = db_session.query(Usuario).get(session['user_id']) + + # Filtrar pagamentos baseado na instância do usuário + if user.has_permission(Permission.REGISTER_CC_PAYMENT): + # Usuário do CC pode ver todos os pagamentos + pagamentos = db_session.query(Pagamento).all() + elif user.has_permission(Permission.REGISTER_CR_PAYMENT): + # Usuário do CR pode ver pagamentos do CR e setores/células abaixo + pagamentos = db_session.query(Pagamento).filter( + Pagamento.cr_id == user.cr_id + ).all() + elif user.has_permission(Permission.REGISTER_SECTOR_PAYMENT): + # Usuário do setor pode ver pagamentos do setor e células abaixo + pagamentos = db_session.query(Pagamento).filter( + Pagamento.setor_id == user.setor_id + ).all() + elif user.has_permission(Permission.REGISTER_CELL_PAYMENT): + # Usuário da célula pode ver apenas pagamentos da célula + pagamentos = db_session.query(Pagamento).filter( + Pagamento.celula_id == user.celula_id + ).all() + else: + flash('Você não tem permissão para visualizar pagamentos.', 'error') + return redirect(url_for('home')) + return render_template("listar_pagamentos.html", pagamentos=pagamentos) # Rota para criar um novo material vendido @app.route("/materiais/novo", methods=["GET", "POST"]) -@login_required -@session_timeout +@require_login +@require_permission(Permission.VIEW_CELL_REPORTS) def novo_material(): if request.method == "POST": materiais_vendidos = MaterialVendido( @@ -429,16 +489,17 @@ def novo_material(): # Rota para listar materiais vendidos @app.route("/materiais") -@login_required -@session_timeout +@require_login +@require_permission(Permission.VIEW_CELL_REPORTS) def listar_materiais(): + """Lista todos os materiais""" materiais = db_session.query(MaterialVendido).all() return render_template("listar_materiais.html", materiais=materiais) # Rota para criar uma nova venda de jornais avulsos @app.route("/jornais/novo", methods=["GET", "POST"]) -@login_required -@session_timeout +@require_login +@require_permission(Permission.VIEW_CELL_REPORTS) def nova_venda_jornal(): if request.method == "POST": vendas_jornais_avulsos = VendaJornalAvulso( @@ -462,16 +523,17 @@ def nova_venda_jornal(): # Rota para listar vendas de jornais avulsos @app.route("/jornais") -@login_required -@session_timeout +@require_login +@require_permission(Permission.VIEW_CELL_REPORTS) def listar_vendas_jornal(): + """Lista todas as vendas de jornal""" vendas = db_session.query(VendaJornalAvulso).all() return render_template("listar_vendas_jornal.html", vendas=vendas) # Rota para criar uma nova assinatura anual @app.route("/assinaturas/novo", methods=["GET", "POST"]) -@login_required -@session_timeout +@require_login +@require_permission(Permission.VIEW_CELL_REPORTS) def nova_assinatura(): if request.method == "POST": assinaturas_anuais = AssinaturaAnual( @@ -497,16 +559,17 @@ def nova_assinatura(): # Rota para listar assinaturas anuais @app.route("/assinaturas") -@login_required -@session_timeout +@require_login +@require_permission(Permission.VIEW_CELL_REPORTS) def listar_assinaturas(): + """Lista todas as assinaturas""" assinaturas = db_session.query(AssinaturaAnual).all() return render_template("listar_assinaturas.html", assinaturas=assinaturas) # Rota para criar um novo relatório de cotas mensais @app.route("/relatorios/cotas/novo", methods=["GET", "POST"]) -@login_required -@session_timeout +@require_login +@require_permission(Permission.VIEW_CELL_REPORTS) def novo_relatorio_cotas(): if request.method == "POST": relatorio_cotas_mensais = RelatorioCotasMensais( @@ -530,16 +593,16 @@ def novo_relatorio_cotas(): # Rota para listar relatórios de cotas mensais @app.route("/relatorios/cotas") -@login_required -@session_timeout +@require_login +@require_permission(Permission.VIEW_CELL_REPORTS) def listar_relatorios_cotas(): relatorios = db_session.query(RelatorioCotasMensais).all() return render_template("listar_relatorios_cotas.html", relatorios=relatorios) # Rota para criar um novo relatório de vendas de materiais @app.route("/relatorios/vendas/novo", methods=["GET", "POST"]) -@login_required -@session_timeout +@require_login +@require_permission(Permission.VIEW_CELL_REPORTS) def novo_relatorio_vendas(): if request.method == "POST": relatorio_vendas_materiais = RelatorioVendasMateriais( @@ -563,15 +626,15 @@ def novo_relatorio_vendas(): # Rota para listar relatórios de vendas de materiais @app.route("/relatorios/vendas") -@login_required -@session_timeout +@require_login +@require_permission(Permission.VIEW_CELL_REPORTS) def listar_relatorios_vendas(): relatorios = db_session.query(RelatorioVendasMateriais).all() return render_template("listar_relatorios_vendas.html", relatorios=relatorios) -@app.route("/militantes/editar/", methods=["GET", "POST"]) -@login_required -@session_timeout +@app.route('/militantes//editar', methods=['GET', 'POST']) +@require_login +@require_permission(Permission.MANAGE_CELL_MEMBERS) def editar_militante(id): militante = db_session.query(Militante).get(id) if not militante: @@ -602,106 +665,525 @@ def editar_militante(id): return render_template("editar_militante.html", militante=militante) -# Rota para criar novo usuário -@app.route("/usuarios/novo", methods=["GET", "POST"]) -@login_required -def novo_usuario(): - if not session.get('is_admin'): - flash('Acesso negado. Apenas administradores podem criar novos usuários.', 'danger') - return redirect(url_for('home')) +@app.route('/dash') +@require_login +def dashboard_admin(): + # Filtrar usuários baseado na hierarquia + if current_user.has_permission('system_config'): + # Secretário Geral e Secretário de Organização podem ver tudo + users = db_session.query(Usuario).all() + elif current_user.has_permission('manage_cc_crs'): + # Membro do CC pode ver tudo + users = db_session.query(Usuario).all() + elif current_user.has_permission('manage_cr_sectors'): + # Secretário de CR pode ver apenas membros do seu CR + users = db_session.query(Usuario).join(Usuario.roles).filter(Role.nome == 'membro_setor').all() + elif current_user.has_permission('manage_sector_cells'): + # Secretário de Setor pode ver apenas membros do seu setor + users = db_session.query(Usuario).join(Usuario.roles).filter(Role.nome == 'membro_celula').all() + elif current_user.has_permission('manage_cell_members'): + # Secretário de Célula pode ver apenas membros da sua célula + users = db_session.query(Usuario).join(Usuario.roles).filter(Role.nome == 'militante_basico').all() + else: + # Militante básico pode ver apenas a si mesmo + users = [current_user] + + return render_template('dashboard.html', users=users) - if request.method == "POST": - username = request.form.get("username") - email = request.form.get("email") - password = request.form.get("password") - confirm_password = request.form.get("confirm_password") - is_admin = bool(request.form.get("is_admin")) +@app.route("/usuarios//otp/reset", methods=["POST"]) +@require_login +@require_permission('system_config') +def reset_otp(user_id): + """Resetar OTP de um usuário""" + db = get_db_connection() + try: + user = db.query(Usuario).get(user_id) + if not user: + return jsonify({'success': False, 'message': 'Usuário não encontrado'}), 404 + + # Gerar novo OTP + user.otp_secret = pyotp.random_base32() + db.commit() + + return jsonify({'success': True, 'message': 'OTP resetado com sucesso'}) + finally: + db.close() - # Validações - if password != confirm_password: - flash('As senhas não conferem.', 'danger') - return render_template('novo_usuario.html') - - # Verificar se usuário já existe - if db_session.query(Usuario).filter_by(username=username).first(): - flash('Nome de usuário já existe.', 'danger') - return render_template('novo_usuario.html') - - if db_session.query(Usuario).filter_by(email=email).first(): - flash('E-mail já cadastrado.', 'danger') - return render_template('novo_usuario.html') - - # Criar novo usuário +@app.route("/usuarios//password/reset", methods=["POST"]) +@require_login +@require_permission('system_config') +def reset_password(user_id): + """Resetar senha de um usuário""" + db = get_db_connection() + try: + user = db.query(Usuario).get(user_id) + if not user: + return jsonify({'success': False, 'message': 'Usuário não encontrado'}), 404 + + # Gerar nova senha aleatória + new_password = ''.join(random.choices(string.ascii_letters + string.digits, k=8)) + user.password_hash = generate_password_hash(new_password) + db.commit() + + # Enviar email com a nova senha try: - novo_usuario = Usuario( - username=username, - password=password, - is_admin=is_admin + msg = Message( + 'Sua senha foi resetada', + recipients=[user.email], + body=f''' + Olá {user.username}, + + Sua senha foi resetada pelo administrador do sistema. + + Sua nova senha é: {new_password} + + Por favor, altere sua senha após o primeiro login. + + Atenciosamente, + Sistema de Controle OCI + ''' ) - novo_usuario.email = email - - db_session.add(novo_usuario) - db_session.commit() - - # Gerar QR code usando a função do create_admin.py - qr_uri = novo_usuario.get_otp_uri() - qr_path = generate_qr_code(novo_usuario) - - flash('Usuário criado com sucesso!', 'success') - return render_template('mostrar_qr_code.html', qr_uri=qr_uri) - + mail.send(msg) + return jsonify({'success': True, 'message': 'Senha resetada e enviada por email'}) except Exception as e: - db_session.rollback() - flash('Erro ao criar usuário. Por favor, tente novamente.', 'danger') - print(f"Erro: {e}") - return render_template('novo_usuario.html') - - return render_template('novo_usuario.html') + db.rollback() + return jsonify({'success': False, 'message': f'Erro ao enviar email: {str(e)}'}), 500 + + finally: + db.close() @app.route('/check_session') def check_session(): - if 'last_activity' not in session: - return jsonify({'expired': True}) - - last_activity = datetime.fromtimestamp(session['last_activity']) - now = datetime.now() + if 'user_id' not in session: + return jsonify({'status': 'expired'}) - if now - last_activity > timedelta(hours=2): - # Registrar o logout por timeout - try: - user = db_session.query(Usuario).get(session.get('user_id')) - if user: - user.ultimo_logout = datetime.now() - user.motivo_logout = "Timeout de sessão" - db_session.commit() - except Exception as e: - print(f"Erro ao registrar logout por timeout: {e}") + db = get_db_connection() + try: + user = db.get(Usuario, session['user_id']) + if not user or user.is_session_expired(): + user.logout() + db.commit() + session.pop('user_id', None) + return jsonify({'status': 'expired'}) - session.clear() - return jsonify({'expired': True}) - - return jsonify({'expired': False}) + user.update_last_activity() + db.commit() + return jsonify({'status': 'active'}) + finally: + db.close() @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()) + db = get_db_connection() + try: + user = db.query(Usuario).filter_by(username='admin').first() + if not user: + flash('Usuário não encontrado', 'error') + return redirect(url_for('login')) + + qr_uri = user.get_otp_uri() + return render_template('mostrar_qr_code.html', qr_uri=qr_uri) + finally: + db.close() # Adicionar nova rota para API de setores @app.route("/api/setores/") -@login_required +@require_login def get_setores(cr_id): setores = db_session.query(Setor).filter_by(cr_id=cr_id).all() return jsonify({ 'setores': [{'id': s.id, 'nome': s.nome} for s in setores] }) +@app.route('/celulas//militantes') +@require_login +def list_militantes_celula(celula_id): + @require_instance_access('celula', celula_id) + def decorated_function(celula_id): + db = get_db_connection() + try: + militantes = db.query(Usuario).filter_by(celula_id=celula_id).all() + return render_template('militantes/list.html', militantes=militantes) + finally: + db.close() + return decorated_function(celula_id) + +@app.route('/setores//militantes') +@require_login +def list_militantes_setor(setor_id): + @require_instance_access('setor', setor_id) + def decorated_function(setor_id): + db = get_db_connection() + try: + militantes = db.query(Usuario).filter_by(setor_id=setor_id).all() + return render_template('militantes/list.html', militantes=militantes) + finally: + db.close() + return decorated_function(setor_id) + +@app.route('/crs//militantes') +@require_login +def list_militantes_cr(cr_id): + @require_instance_access('cr', cr_id) + def decorated_function(cr_id): + db = get_db_connection() + try: + militantes = db.query(Usuario).filter_by(cr_id=cr_id).all() + return render_template('militantes/list.html', militantes=militantes) + finally: + db.close() + return decorated_function(cr_id) + +@app.route('/celulas//pagamentos') +@require_login +def list_pagamentos_celula(celula_id): + @require_instance_access('celula', celula_id) + def decorated_function(celula_id): + db = get_db_connection() + try: + pagamentos = db.query(Pagamento).filter_by(celula_id=celula_id).all() + return render_template('pagamentos/list.html', pagamentos=pagamentos) + finally: + db.close() + return decorated_function(celula_id) + +@app.route('/setores//pagamentos') +@require_login +def list_pagamentos_setor(setor_id): + @require_instance_access('setor', setor_id) + def decorated_function(setor_id): + db = get_db_connection() + try: + pagamentos = db.query(Pagamento).join(Usuario).filter(Usuario.setor_id == setor_id).all() + return render_template('pagamentos/list.html', pagamentos=pagamentos) + finally: + db.close() + return decorated_function(setor_id) + +@app.route('/crs//pagamentos') +@require_login +def list_pagamentos_cr(cr_id): + @require_instance_access('cr', cr_id) + def decorated_function(cr_id): + db = get_db_connection() + try: + pagamentos = db.query(Pagamento).join(Usuario).filter(Usuario.cr_id == cr_id).all() + return render_template('pagamentos/list.html', pagamentos=pagamentos) + finally: + db.close() + return decorated_function(cr_id) + +@app.route('/celulas//pagamentos/novo', methods=['GET', 'POST']) +@require_login +@require_instance_permission('REGISTER_CELL_PAYMENT') +def novo_pagamento_celula(celula_id): + if request.method == 'POST': + db = get_db_connection() + try: + pagamento = Pagamento( + valor=request.form['valor'], + data=request.form['data'], + militante_id=request.form['militante_id'], + celula_id=celula_id + ) + db.add(pagamento) + db.commit() + flash('Pagamento registrado com sucesso!', 'success') + return redirect(url_for('list_pagamentos_celula', celula_id=celula_id)) + finally: + db.close() + return render_template('pagamentos/form.html') + +@app.route('/setores//pagamentos/novo', methods=['GET', 'POST']) +@require_login +@require_instance_permission('REGISTER_SECTOR_PAYMENT') +def novo_pagamento_setor(setor_id): + if request.method == 'POST': + db = get_db_connection() + try: + pagamento = Pagamento( + valor=request.form['valor'], + data=request.form['data'], + militante_id=request.form['militante_id'], + setor_id=setor_id + ) + db.add(pagamento) + db.commit() + flash('Pagamento registrado com sucesso!', 'success') + return redirect(url_for('list_pagamentos_setor', setor_id=setor_id)) + finally: + db.close() + return render_template('pagamentos/form.html') + +@app.route('/crs//pagamentos/novo', methods=['GET', 'POST']) +@require_login +@require_instance_permission('REGISTER_CR_PAYMENT') +def novo_pagamento_cr(cr_id): + if request.method == 'POST': + db = get_db_connection() + try: + pagamento = Pagamento( + valor=request.form['valor'], + data=request.form['data'], + militante_id=request.form['militante_id'], + cr_id=cr_id + ) + db.add(pagamento) + db.commit() + flash('Pagamento registrado com sucesso!', 'success') + return redirect(url_for('list_pagamentos_cr', cr_id=cr_id)) + finally: + db.close() + return render_template('pagamentos/form.html') + +@app.route("/alterar_senha", methods=["GET", "POST"]) +@require_login +def alterar_senha(): + """Rota para alterar a senha do usuário""" + if request.method == "POST": + senha_atual = request.form.get("senha_atual") + nova_senha = request.form.get("nova_senha") + confirmar_senha = request.form.get("confirmar_senha") + + if not all([senha_atual, nova_senha, confirmar_senha]): + flash("Todos os campos são obrigatórios.", "error") + return redirect(url_for("alterar_senha")) + + if nova_senha != confirmar_senha: + flash("As senhas não coincidem.", "error") + return redirect(url_for("alterar_senha")) + + db = get_db_connection() + try: + user = db.query(Usuario).get(current_user.id) + if not user.check_password(senha_atual): + flash("Senha atual incorreta.", "error") + return redirect(url_for("alterar_senha")) + + user.password_hash = generate_password_hash(nova_senha) + db.commit() + flash("Senha alterada com sucesso!", "success") + return redirect(url_for("home")) + finally: + db.close() + + return render_template("alterar_senha.html") + +@app.route('/usuarios//toggle_status', methods=['POST']) +@require_login +def toggle_user_status(user_id): + user = db_session.query(Usuario).get_or_404(user_id) + + # Verificar permissões baseado na hierarquia + if not current_user.has_permission('system_config'): + if current_user.has_permission('manage_cr_sectors'): + # Secretário de CR só pode gerenciar membros do seu CR + if user.cr_id != current_user.cr_id: + flash('Você não tem permissão para gerenciar este usuário.', 'danger') + return redirect(url_for('dashboard_admin')) + elif current_user.has_permission('manage_sector_cells'): + # Secretário de Setor só pode gerenciar membros do seu setor + if user.setor_id != current_user.setor_id: + flash('Você não tem permissão para gerenciar este usuário.', 'danger') + return redirect(url_for('dashboard_admin')) + elif current_user.has_permission('manage_cell_members'): + # Secretário de Célula só pode gerenciar membros da sua célula + if user.celula_id != current_user.celula_id: + flash('Você não tem permissão para gerenciar este usuário.', 'danger') + return redirect(url_for('dashboard_admin')) + else: + # Militante básico não pode gerenciar ninguém + flash('Você não tem permissão para gerenciar usuários.', 'danger') + return redirect(url_for('dashboard_admin')) + + user.ativo = not user.ativo + db_session.commit() + + return jsonify({'success': True, 'message': 'Status do usuário alterado com sucesso!'}) + +@app.route('/usuarios//alterar_nivel', methods=['POST']) +@require_login +def alterar_nivel(user_id): + user = db_session.query(Usuario).get_or_404(user_id) + novo_nivel = request.json.get('nivel') + + # Verificar permissões baseado na hierarquia + if not current_user.has_permission('system_config'): + if current_user.has_permission('manage_cr_sectors'): + # Secretário de CR só pode alterar níveis dentro do seu CR + if user.cr_id != current_user.cr_id: + return jsonify({'success': False, 'message': 'Você não tem permissão para alterar o nível deste usuário.'}) + elif current_user.has_permission('manage_sector_cells'): + # Secretário de Setor só pode alterar níveis dentro do seu setor + if user.setor_id != current_user.setor_id: + return jsonify({'success': False, 'message': 'Você não tem permissão para alterar o nível deste usuário.'}) + else: + # Outros níveis não podem alterar níveis + return jsonify({'success': False, 'message': 'Você não tem permissão para alterar níveis de usuários.'}) + + # Verificar se o novo nível é válido para o nível hierárquico do usuário atual + if current_user.has_permission('system_config'): + # Secretário Geral e Secretário de Organização podem alterar para qualquer nível + pass + elif current_user.has_permission('manage_cr_sectors'): + # Secretário de CR só pode alterar para níveis do CR + if novo_nivel not in ['membro_cr', 'secretario_cr']: + return jsonify({'success': False, 'message': 'Nível inválido para este CR.'}) + elif current_user.has_permission('manage_sector_cells'): + # Secretário de Setor só pode alterar para níveis do setor + if novo_nivel not in ['membro_setor', 'secretario_setor']: + return jsonify({'success': False, 'message': 'Nível inválido para este setor.'}) + + # Atualizar o nível do usuário + user.role = novo_nivel + db_session.commit() + + return jsonify({'success': True, 'message': 'Nível do usuário alterado com sucesso!'}) + +@app.route('/usuarios//toggle_quadro_orientador', methods=['POST']) +@require_login +def toggle_quadro_orientador(user_id): + db = get_db_connection() + try: + user = db.query(Usuario).get(user_id) + if not user: + return jsonify({'success': False, 'message': 'Usuário não encontrado'}), 404 + + # Verificar permissões + if not (current_user.has_permission('system_config') or + (current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or + (current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id)): + return jsonify({'success': False, 'message': 'Você não tem permissão para alterar esta responsabilidade'}), 403 + + # Verificar se o usuário tem um militante associado + if not user.militante: + return jsonify({'success': False, 'message': 'Usuário não tem um militante associado'}), 400 + + # Alternar o status de Quadro-Orientador + user.militante.quadro_orientador = not user.militante.quadro_orientador + + # Atualizar a responsabilidade no campo responsabilidades + if user.militante.quadro_orientador: + user.militante.responsabilidades |= Militante.QUADRO_ORIENTADOR + else: + user.militante.responsabilidades &= ~Militante.QUADRO_ORIENTADOR + + db.commit() + return jsonify({ + 'success': True, + 'message': f'Responsabilidade de Quadro-Orientador {"adicionada" if user.militante.quadro_orientador else "removida"} com sucesso' + }) + except Exception as e: + db.rollback() + return jsonify({'success': False, 'message': str(e)}), 500 + finally: + db.close() + +@app.route('/usuarios//toggle_aspirante', methods=['POST']) +@require_login +def toggle_aspirante(user_id): + db = get_db_connection() + try: + user = db.query(Usuario).get(user_id) + if not user: + return jsonify({'success': False, 'message': 'Usuário não encontrado'}), 404 + + # Verificar permissões + if not (current_user.has_permission('system_config') or + (current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or + (current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id)): + return jsonify({'success': False, 'message': 'Você não tem permissão para alterar este status'}), 403 + + # Verificar se o usuário tem um militante associado + if not user.militante: + return jsonify({'success': False, 'message': 'Usuário não tem um militante associado'}), 400 + + # Se estiver tentando remover o status de aspirante + if user.militante.aspirante: + # Verificar se já passaram 3 meses + if datetime.utcnow() - user.militante.data_inicio_aspirante < timedelta(days=90): + return jsonify({ + 'success': False, + 'message': 'Não é possível remover o status de aspirante antes de 3 meses de integração' + }), 400 + + # Verificar se há avaliação + if not user.militante.avaliacao_aspirante: + return jsonify({ + 'success': False, + 'message': 'É necessário registrar uma avaliação antes de remover o status de aspirante' + }), 400 + + # Alternar o status de Aspirante + user.militante.aspirante = not user.militante.aspirante + + # Atualizar a responsabilidade no campo responsabilidades + if user.militante.aspirante: + user.militante.responsabilidades |= Militante.ASPIRANTE + user.militante.data_inicio_aspirante = datetime.utcnow() + user.militante.avaliacao_aspirante = None + user.militante.data_avaliacao_aspirante = None + else: + user.militante.responsabilidades &= ~Militante.ASPIRANTE + user.militante.data_avaliacao_aspirante = datetime.utcnow() + + db.commit() + return jsonify({ + 'success': True, + 'message': f'Status de Aspirante {"adicionado" if user.militante.aspirante else "removido"} com sucesso' + }) + except Exception as e: + db.rollback() + return jsonify({'success': False, 'message': str(e)}), 500 + finally: + db.close() + +@app.route('/usuarios//avaliar_aspirante', methods=['POST']) +@require_login +def avaliar_aspirante(user_id): + db = get_db_connection() + try: + user = db.query(Usuario).get(user_id) + if not user: + return jsonify({'success': False, 'message': 'Usuário não encontrado'}), 404 + + # Verificar permissões + if not (current_user.has_permission('system_config') or + (current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or + (current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id)): + return jsonify({'success': False, 'message': 'Você não tem permissão para avaliar este aspirante'}), 403 + + # Verificar se o usuário tem um militante associado e é aspirante + if not user.militante or not user.militante.aspirante: + return jsonify({'success': False, 'message': 'Usuário não é um aspirante'}), 400 + + # Verificar se já passaram 3 meses + if datetime.utcnow() - user.militante.data_inicio_aspirante < timedelta(days=90): + return jsonify({ + 'success': False, + 'message': 'Não é possível avaliar o aspirante antes de 3 meses de integração' + }), 400 + + # Obter a avaliação do corpo da requisição + avaliacao = request.json.get('avaliacao') + if not avaliacao: + return jsonify({'success': False, 'message': 'A avaliação é obrigatória'}), 400 + + # Atualizar a avaliação + user.militante.avaliacao_aspirante = avaliacao + user.militante.data_avaliacao_aspirante = datetime.utcnow() + + db.commit() + return jsonify({ + 'success': True, + 'message': 'Avaliação registrada com sucesso' + }) + except Exception as e: + db.rollback() + return jsonify({'success': False, 'message': str(e)}), 500 + finally: + db.close() + def create_app(): app = Flask(__name__) # ... existing code ... @@ -709,12 +1191,58 @@ def create_app(): # ... existing code ... return app -# Iniciar o servidor Flask -if __name__ == "__main__": - # Verificar se existe usuário admin e gerar QR code - admin = db_session.query(Usuario).filter_by(username="admin").first() - if admin: - print("\n=== Gerando QR Code para usuário admin existente ===") - generate_qr_code(admin) +def init_system(): + """Inicializa o sistema com todos os usuários necessários""" + print("Inicializando sistema...") + # Criar admin + create_admin() + + # Criar usuários de teste + create_test_users() + + # Verificar configuração + db = get_db_connection() + try: + # Verificar admin + admin = db.query(Usuario).filter_by(username='admin').first() + if admin: + print("\nAdmin configurado:") + print(f"Username: admin") + print(f"Senha: admin123") + if os.path.exists('admin_qr.png'): + print("OTP: Usando configuração existente do arquivo admin_qr.png") + else: + print("OTP: Nova configuração gerada") + + # Verificar usuários de teste + test_users = ['aligner', 'tester', 'deployer'] + for username in test_users: + user = db.query(Usuario).filter_by(username=username).first() + if user: + print(f"\nUsuário {username} configurado:") + print(f"Username: {username}") + print(f"Senha: Test123!@#") + if user.otp_secret: + print("OTP: Configurado") + else: + print("OTP: Não configurado") + finally: + db.close() + + print("\nInstruções:") + print("1. Configure o OTP usando o QR code gerado") + print("2. Faça login com as credenciais fornecidas") + print("3. Altere a senha no primeiro login") + + print("\nCredenciais:") + print("Admin:") + print("Username: admin") + print("Senha: admin123") + print("\nUsuários de teste:") + print("Username: aligner, tester, deployer") + print("Senha: Test123!@#") + +if __name__ == '__main__': + init_system() app.run(debug=True) diff --git a/config.py b/config.py deleted file mode 100644 index a9766b0..0000000 --- a/config.py +++ /dev/null @@ -1 +0,0 @@ -SECRET_KEY = 'sua_chave_secreta_aqui' # Use uma chave segura em produção \ No newline at end of file diff --git a/create_admin.py b/create_admin.py index 626221f..c994bf7 100644 --- a/create_admin.py +++ b/create_admin.py @@ -1,136 +1,91 @@ -from functions.database import init_database, Usuario, get_db_connection -import qrcode import os -from pathlib import Path +from functions.database import get_db_connection, Usuario +from functions.rbac import Role import pyotp +import qrcode +import base64 +from io import BytesIO -def generate_qr_code(user): - """ - Gera o QR code para um usuário específico - - Args: - user: Instância do modelo Usuario - - Returns: - Path: Caminho do arquivo QR code gerado - """ - import qrcode - - # Gerar QR Code apenas na raiz do projeto - qr_path = Path('admin_qr.png') - - # Remover arquivo antigo se existir - if qr_path.exists(): - os.remove(str(qr_path)) - - # Gerar e salvar QR Code - qr = qrcode.QRCode(version=1, box_size=10, border=5) - qr.add_data(user.get_otp_uri()) - qr.make(fit=True) - img = qr.make_image(fill_color="black", back_color="white") - img.save(str(qr_path)) - - print(f"\nQR Code gerado em: {os.path.abspath(qr_path)}") - - return qr_path - -def create_admin_user(): +def create_admin(): + """Cria o usuário admin se não existir""" + db = get_db_connection() try: - # Inicializar o banco de dados - init_database() - - # Obter a sessão - db_session = get_db_connection() - - # Verificar se o admin foi criado - admin = db_session.query(Usuario).filter_by(username="admin").first() + # Verificar se o admin já existe + admin = db.query(Usuario).filter_by(username='admin').first() if admin: - print("\n=== Usuário Admin Encontrado ===") - # Atualizar QR code mesmo se o usuário já existir - qr_path = generate_qr_code(admin) + print("Usuário admin já existe") + + # Verificar se o arquivo admin_qr.png existe + if os.path.exists('admin_qr.png'): + print("Usando OTP existente do arquivo admin_qr.png") + # Extrair o OTP secret do QR code existente + with open('admin_qr.png', 'rb') as f: + qr_data = f.read() + # Aqui você precisaria implementar a lógica para extrair o OTP secret do QR code + # Por enquanto, vamos apenas manter o OTP existente + return + else: + print("Gerando novo OTP para o admin...") + # Gerar novo OTP + otp_secret = pyotp.random_base32() + admin.otp_secret = otp_secret + db.commit() else: - print("\n=== Criando Novo Usuário Admin ===") - # Criar usuário admin com novo segredo OTP + print("Criando usuário admin...") + # Criar usuário admin admin = Usuario( - username="admin", - password="admin123", + username='admin', + password='admin123', is_admin=True ) - admin.email = "admin@example.com" + admin.email = 'admin@controles.com' + db.add(admin) + db.commit() - # Adicionar e fazer commit para obter o ID - db_session.add(admin) - db_session.commit() + # Gerar OTP + otp_secret = pyotp.random_base32() + admin.otp_secret = otp_secret + db.commit() - # Recarregar o usuário do banco para garantir dados atualizados - admin = db_session.query(Usuario).filter_by(username="admin").first() + # Atribuir role de Secretário Geral + admin_role = db.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first() + if admin_role: + admin.roles.append(admin_role) + db.commit() - # Verificar e mostrar informações do OTP - print("\n=== Informações do Usuário Admin ===") - print(f"Username: admin") - print(f"Senha: admin123") - print(f"Email: {admin.email}") - print(f"Segredo OTP atual: {admin.otp_secret}") + # Gerar QR code + totp = pyotp.TOTP(otp_secret) + provisioning_uri = totp.provisioning_uri(admin.username, issuer_name="Sistema de Controles") - # Gerar URI do OTP usando o segredo armazenado - totp = pyotp.TOTP(admin.otp_secret) - otp_uri = totp.provisioning_uri( - name=admin.username, - issuer_name="Sistema de Controles" - ) + qr = qrcode.QRCode(version=1, box_size=10, border=5) + qr.add_data(provisioning_uri) + qr.make(fit=True) - # Usar a função extraída para gerar o QR code - qr_path = generate_qr_code(admin) + img = qr.make_image(fill_color="black", back_color="white") - print("\n=== QR Code Gerado ===") - print(f"QR Code salvo em: {qr_path}") - print(f"URI do OTP: {otp_uri}") + # Salvar QR code como base64 + buffered = BytesIO() + img.save(buffered, format="PNG") + qr_base64 = base64.b64encode(buffered.getvalue()).decode() - # Gerar código atual para verificação - current_code = totp.now() - print("\n=== Verificação do OTP ===") - print(f"Código OTP atual: {current_code}") - print(f"Verificação do código: {totp.verify(current_code)}") + # Salvar QR code como arquivo + img.save('admin_qr.png') - print("\n=== Instruções para Configuração ===") - print("1. Instale um aplicativo autenticador no seu celular") - print(" (Google Authenticator, Microsoft Authenticator, etc)") - print("2. Abra o aplicativo") - print("3. Selecione a opção para adicionar uma nova conta") - print("4. Escaneie o QR Code salvo em:", qr_path) - print("\nOU configure manualmente:") - print(f"- Nome da conta: {admin.username}") - print(f"- Segredo: {admin.otp_secret}") - print("- Tipo: Baseado em tempo (TOTP)") - print("- Algoritmo: SHA1") - print("- Dígitos: 6") - print("- Intervalo: 30 segundos") - - # Verificação final - print("\n=== Teste de Verificação ===") - test_code = totp.now() - print(f"Código de teste: {test_code}") - is_valid = admin.verify_otp(test_code) - print(f"Verificação do código: {'Sucesso' if is_valid else 'Falha'}") - - if not is_valid: - print("\nALERTA: Verificação do OTP falhou!") - print("Por favor, verifique se o segredo OTP está correto.") + print("\nConfiguração do OTP para o admin:") + print(f"OTP Secret: {otp_secret}") + print("\nInstruções:") + print("1. Use um aplicativo autenticador (como Google Authenticator ou Authy)") + print("2. Escaneie o QR code ou insira o OTP Secret manualmente") + print("3. Use o código gerado para fazer login") + print("\nQR code salvo em 'admin_qr.png'") except Exception as e: - print(f"\nErro durante a execução: {e}") - import traceback - traceback.print_exc() + print(f"Erro ao criar admin: {str(e)}") + db.rollback() + raise finally: - if 'db_session' in locals(): - db_session.close() + db.close() -if __name__ == "__main__": - # Remover banco de dados existente para começar limpo - db_path = Path.home() / '.local' / 'share' / 'controles' / 'database.db' - if db_path.exists(): - os.remove(db_path) - print("Banco de dados antigo removido.") - - create_admin_user() \ No newline at end of file +if __name__ == '__main__': + create_admin() \ No newline at end of file diff --git a/create_test_users.py b/create_test_users.py new file mode 100644 index 0000000..51e0e39 --- /dev/null +++ b/create_test_users.py @@ -0,0 +1,104 @@ +from functions.database import get_db_connection, Usuario +from functions.rbac import Role +import pyotp +import qrcode +import os +import base64 +from io import BytesIO + +def create_test_users(): + """Cria usuários de teste se não existirem""" + db = get_db_connection() + try: + # Usuários de teste + test_users = [ + { + 'username': 'teste', + 'password': 'admin123', # Mesma senha do admin + 'email': 'teste@controles.com', + 'is_admin': True + }, + { + 'username': 'aligner', + 'password': 'Test123!@#', + 'email': 'aligner@controles.com', + 'is_admin': False + }, + { + 'username': 'tester', + 'password': 'Test123!@#', + 'email': 'tester@controles.com', + 'is_admin': False + }, + { + 'username': 'deployer', + 'password': 'Test123!@#', + 'email': 'deployer@controles.com', + 'is_admin': False + } + ] + + # Obter o OTP secret do admin se existir + admin = db.query(Usuario).filter_by(username='admin').first() + admin_otp_secret = admin.otp_secret if admin else None + + for user_data in test_users: + # Verificar se o usuário já existe + user = db.query(Usuario).filter_by(username=user_data['username']).first() + + if not user: + print(f"Criando usuário {user_data['username']}...") + # Criar usuário + user = Usuario( + username=user_data['username'], + password=user_data['password'], + is_admin=user_data['is_admin'] + ) + user.email = user_data['email'] + db.add(user) + db.commit() + + # Se for o usuário teste, usar o mesmo OTP do admin + if user_data['username'] == 'teste' and admin_otp_secret: + user.otp_secret = admin_otp_secret + db.commit() + else: + # Gerar novo OTP para outros usuários + otp_secret = pyotp.random_base32() + user.otp_secret = otp_secret + db.commit() + + # Atribuir role de Secretário Geral para o usuário teste + if user_data['username'] == 'teste': + admin_role = db.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first() + if admin_role: + user.roles.append(admin_role) + db.commit() + + print(f"Usuário {user_data['username']} criado com sucesso!") + else: + print(f"Usuário {user_data['username']} já existe") + + # Se for o usuário teste e não tiver o OTP do admin, atualizar + if user_data['username'] == 'teste' and admin_otp_secret and user.otp_secret != admin_otp_secret: + user.otp_secret = admin_otp_secret + db.commit() + print(f"OTP do usuário teste atualizado para o mesmo do admin") + + # Verificar se o usuário teste tem a role de Secretário Geral + if user_data['username'] == 'teste': + admin_role = db.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first() + if admin_role and admin_role not in user.roles: + user.roles.append(admin_role) + db.commit() + print(f"Role de Secretário Geral atribuída ao usuário teste") + + except Exception as e: + print(f"Erro ao criar usuários de teste: {str(e)}") + db.rollback() + raise + finally: + db.close() + +if __name__ == '__main__': + create_test_users() \ No newline at end of file diff --git a/dao.py b/dao.py deleted file mode 100644 index 98b37c6..0000000 --- a/dao.py +++ /dev/null @@ -1,8 +0,0 @@ -from functions.database import execute_query - -def get_user_by_email(email): - query = "SELECT * FROM users WHERE email = %s" - cursor = execute_query(query, (email,)) - if cursor: - return cursor.fetchone() - return None diff --git a/database.sql b/database.sql deleted file mode 100644 index d471a92..0000000 --- a/database.sql +++ /dev/null @@ -1,99 +0,0 @@ --- Tabela de Militantes -CREATE TABLE militantes ( - id INT PRIMARY KEY AUTO_INCREMENT, - nome VARCHAR(100) NOT NULL, - cpf VARCHAR(14) UNIQUE, - email VARCHAR(100) UNIQUE, - telefone VARCHAR(15), - endereco VARCHAR(255), - filiado BOOLEAN DEFAULT false -); - --- Tabela de Cotas Mensais -CREATE TABLE cotas_mensais ( - id INT PRIMARY KEY AUTO_INCREMENT, - militante_id INT, - valor_antigo DECIMAL(10, 2) NOT NULL, - valor_novo DECIMAL(10, 2) NOT NULL, - data_alteracao DATE NOT NULL, - FOREIGN KEY (militante_id) REFERENCES militantes(id) -); - --- Tabela de Pagamentos -CREATE TABLE tipos_pagamento ( - id INT PRIMARY KEY AUTO_INCREMENT, - descricao VARCHAR(100) NOT NULL -); - -CREATE TABLE pagamentos ( - id INT PRIMARY KEY AUTO_INCREMENT, - militante_id INT, - tipo_pagamento_id INT, - valor DECIMAL(10, 2) NOT NULL, - data_pagamento DATE NOT NULL, - FOREIGN KEY (militante_id) REFERENCES militantes(id), - FOREIGN KEY (tipo_pagamento_id) REFERENCES tipos_pagamento(id) -); - --- Tabela de Tipos de Materiais -CREATE TABLE tipos_materiais ( - id INT PRIMARY KEY AUTO_INCREMENT, - descricao VARCHAR(100) NOT NULL -); - --- Tabela de Materiais Vendidos -CREATE TABLE materiais_vendidos ( - id INT PRIMARY KEY AUTO_INCREMENT, - militante_id INT, - tipo_material_id INT, - descricao VARCHAR(255) NOT NULL, - valor DECIMAL(10, 2) NOT NULL, - data_venda DATE NOT NULL, - FOREIGN KEY (militante_id) REFERENCES militantes(id), - FOREIGN KEY (tipo_material_id) REFERENCES tipos_materiais(id) -); - --- Tabela de Vendas de Jornais Avulsos -CREATE TABLE vendas_jornais_avulsos ( - id INT PRIMARY KEY AUTO_INCREMENT, - militante_id INT, - quantidade INT NOT NULL, - valor_total DECIMAL(10, 2) NOT NULL, - data_venda DATE NOT NULL, - FOREIGN KEY (militante_id) REFERENCES militantes(id) -); - --- Tabela de Assinaturas Anuais -CREATE TABLE assinaturas_anuais ( - id INT PRIMARY KEY AUTO_INCREMENT, - militante_id INT, - tipo_material_id INT, - quantidade INT NOT NULL, - valor_total DECIMAL(10, 2) NOT NULL, - data_inicio DATE NOT NULL, - data_fim DATE NOT NULL, - FOREIGN KEY (militante_id) REFERENCES militantes(id), - FOREIGN KEY (tipo_material_id) REFERENCES tipos_materiais(id) -); - --- Tabela de Relatório de Cotas Mensais -CREATE TABLE relatorio_cotas_mensais ( - id INT PRIMARY KEY AUTO_INCREMENT, - setor_id INT, - comite_id INT, - total_cotas DECIMAL(10, 2) NOT NULL, - data_relatorio DATE NOT NULL, - FOREIGN KEY (setor_id) REFERENCES setores(id), - FOREIGN KEY (comite_id) REFERENCES comites_centrais(id) -); - --- Tabela de Relatório de Vendas de Materiais -CREATE TABLE relatorio_vendas_materiais ( - id INT PRIMARY KEY AUTO_INCREMENT, - setor_id INT, - comite_id INT, - total_vendas DECIMAL(10, 2) NOT NULL, - data_relatorio DATE NOT NULL, - FOREIGN KEY (setor_id) REFERENCES setores(id), - FOREIGN KEY (comite_id) REFERENCES comites_centrais(id) -); diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..89389c9 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,165 @@ +# Sistema de Controle OCI + +## Hierarquia e Permissões + +### Níveis de Acesso + +1. **Militante Básico** + - Pode ver apenas os membros da sua própria célula + - Não pode alterar níveis de outros usuários + +2. **Secretário de Célula** + - Pode ver e gerenciar apenas os membros da sua célula + - Não pode alterar níveis de outros usuários + +3. **Membro de Setor** + - Pode ver apenas os dados do setor ao qual pertence + - Não pode alterar níveis de outros usuários + +4. **Secretário de Setor** + - Pode ver e gerenciar todos os dados do seu setor + - Pode alterar níveis de militantes do setor, transformando-os em secretários + - Não pode alterar níveis de membros de outros setores + +5. **Membro de CR** + - Pode ver apenas os dados do CR ao qual pertence + - Não pode alterar níveis de outros usuários + +6. **Secretário de CR** + - Pode ver e gerenciar todos os dados do seu CR + - Pode alterar níveis de membros do CR + - Não pode alterar níveis de membros de outros CRs + +7. **Membro do CC** + - Pode ver todos os dados do sistema + - Não pode alterar níveis de outros usuários + +8. **Secretário Geral e Secretário de Organização** + - Pode ver todos os dados do sistema + - Pode alterar níveis de qualquer usuário em qualquer instância + +### Regras de Visualização + +- Cada militante só pode ver os membros da sua própria célula +- Membros de setor só veem dados do setor ao qual pertencem +- Membros de CR só veem informações do CR ao qual pertencem +- Membros do CC podem ver todas as informações do sistema + +### Regras de Edição + +- Apenas o Secretário Geral e o Secretário de Organização podem alterar níveis em todas as instâncias +- Secretários de CR podem alterar níveis apenas dentro do seu CR +- Secretários de Setor podem alterar níveis apenas dentro do seu setor, transformando militantes em secretários +- Outros níveis não podem alterar níveis de outros usuários + +## Responsabilidades + +O sistema suporta as seguintes responsabilidades para militantes: + +- Militante Básico (1) +- Secretário de Célula (2) +- Secretário de Setor (4) +- Secretário de CR (8) +- Secretário de CC (16) +- Secretário Geral (32) +- Quadro-Orientador (64) +- Responsável de Finanças (256) +- Responsável de Imprensa (512) + +### Status de Aspirante + +Todo novo militante começa como Aspirante. Este status tem as seguintes características: + +1. **Duração Mínima**: O status de Aspirante deve ser mantido por pelo menos 3 meses após a integração do militante. + +2. **Avaliação Obrigatória**: Para remover o status de Aspirante, é necessário: + - Ter passado o período mínimo de 3 meses + - Registrar uma avaliação detalhada da atuação do militante durante este período + +3. **Quem pode Avaliar**: A avaliação e remoção do status de Aspirante pode ser feita por: + - Secretário Geral + - Secretário de Organização + - Secretários de CR (para militantes de seu CR) + - Secretários de Setor (para militantes de seu setor) + +4. **Registro da Avaliação**: A avaliação deve incluir: + - Análise da participação do militante nas atividades + - Desenvolvimento político e organizativo + - Pontos fortes e aspectos a melhorar + - Recomendações para o desenvolvimento futuro + +5. **Histórico**: O sistema mantém registro de: + - Data de início do período como Aspirante + - Data da avaliação + - Texto completo da avaliação + +O Quadro-Orientador é uma responsabilidade especial que pode ser atribuída a militantes em qualquer nível hierárquico, incluindo membros de CR e CC. Esta responsabilidade indica que o militante tem a função de orientar e apoiar outros militantes em sua formação política e organizativa. + +A atribuição da responsabilidade de Quadro-Orientador pode ser feita por: +- Secretário Geral +- Secretário de Organização +- Secretários de CR (para militantes de seu CR) +- Secretários de Setor (para militantes de seu setor) + +### Responsáveis de Finanças e Imprensa + +Cada instância (Célula, Setor, CR e CC) possui três responsáveis: + +1. **Responsável Geral**: Obrigatório para todas as instâncias. É o principal responsável pela instância. + +2. **Responsável de Finanças**: Opcional. Responsável por: + - Controle financeiro da instância + - Arrecadação de contribuições + - Prestação de contas + - Planejamento financeiro + +3. **Responsável de Imprensa**: Opcional. Responsável por: + - Comunicação externa da instância + - Produção de materiais de divulgação + - Gestão de redes sociais + - Relacionamento com a mídia + +Os responsáveis de finanças e imprensa são designados pelo responsável geral da instância, com aprovação da instância superior. + +## Hierarquia de Instâncias + +1. **Comitê Central (CC)** + - Instância máxima da organização + - Possui responsável geral, de finanças e de imprensa + - Coordena todos os CRs + +2. **Comitê Regional (CR)** + - Subordinado ao CC + - Possui responsável geral, de finanças e de imprensa + - Coordena os setores da sua região + +3. **Setor** + - Subordinado ao CR + - Possui responsável geral, de finanças e de imprensa + - Coordena as células do seu setor + +4. **Célula** + - Subordinada ao Setor + - Possui responsável geral, de finanças e de imprensa + - Unidade básica de organização + +## Permissões + +As permissões no sistema são baseadas nas responsabilidades do militante e na hierarquia das instâncias: + +1. **Visualização** + - Militantes básicos veem apenas sua célula + - Secretários de célula veem sua célula + - Secretários de setor veem seu setor e células + - Secretários de CR veem seu CR, setores e células + - Secretários de CC veem todos os dados + +2. **Edição** + - Cada nível pode gerenciar apenas os níveis abaixo + - Responsáveis de finanças e imprensa podem editar apenas suas áreas + - Quadros-Orientadores podem avaliar militantes + +3. **Responsabilidades** + - Apenas o nível superior pode atribuir responsabilidades + - Responsáveis de finanças e imprensa são designados pelo responsável geral + - O status de Quadro-Orientador segue regras específicas \ No newline at end of file diff --git a/docs/rbac.md b/docs/rbac.md new file mode 100644 index 0000000..27f6fe3 --- /dev/null +++ b/docs/rbac.md @@ -0,0 +1,239 @@ +# Sistema de Permissões RBAC (Role-Based Access Control) + +## Níveis de Permissão + +O sistema de permissões é hierárquico, onde cada nível herda as permissões do nível anterior. A hierarquia é a seguinte (do menor para o maior nível): + +### 1. Militante Básico +- Acesso apenas aos seus próprios dados +- Visualização de sua célula +- Sem permissões administrativas + +### 2. Secretário de Célula +- Todas as permissões do Militante Básico +- Gerenciamento de militantes da sua célula +- Visualização de dados da célula +- Cadastro de novos militantes na célula + +### 3. Membro de Setor +- Todas as permissões do Secretário de Célula +- Visualização de dados de todas as células do setor +- Acesso a relatórios do setor + +### 4. Secretário de Setor +- Todas as permissões do Membro de Setor +- Gerenciamento de todas as células do setor +- Criação de novas células no setor +- Geração de relatórios do setor +- Gerenciamento de militantes do setor + +### 5. Membro de CR (Comitê Regional) +- Todas as permissões do Secretário de Setor +- Visualização de dados de todos os setores do CR +- Acesso a relatórios do CR + +### 6. Secretário de CR +- Todas as permissões do Membro de CR +- Gerenciamento de todos os setores do CR +- Criação de novos setores no CR +- Geração de relatórios do CR +- Gerenciamento de militantes do CR + +### 7. Membro do CC (Comitê Central) +- Todas as permissões do Secretário de CR +- Visualização de dados de todos os CRs +- Acesso a relatórios nacionais + +### 8. Secretário Geral / Secretário de Organização do CC +- Todas as permissões do Membro do CC +- Gerenciamento de todos os CRs +- Criação de novos CRs +- Geração de relatórios nacionais +- Gerenciamento de todos os militantes +- Configurações do sistema + +## Implementação Técnica + +O sistema RBAC é implementado através de: + +1. **Roles**: Definem os níveis de acesso +2. **Permissions**: Definem as ações permitidas +3. **Role-Permission Mapping**: Mapeia quais permissões cada role possui +4. **User-Role Assignment**: Atribui roles aos usuários + +### Estrutura do Banco de Dados + +```sql +-- Roles +CREATE TABLE roles ( + id INTEGER PRIMARY KEY, + nome VARCHAR(50) UNIQUE NOT NULL, + nivel INTEGER NOT NULL, + descricao TEXT +); + +-- Permissions +CREATE TABLE permissions ( + id INTEGER PRIMARY KEY, + nome VARCHAR(50) UNIQUE NOT NULL, + descricao TEXT +); + +-- Role-Permission Mapping +CREATE TABLE role_permissions ( + role_id INTEGER, + permission_id INTEGER, + PRIMARY KEY (role_id, permission_id), + FOREIGN KEY (role_id) REFERENCES roles(id), + FOREIGN KEY (permission_id) REFERENCES permissions(id) +); + +-- User-Role Assignment +CREATE TABLE user_roles ( + user_id INTEGER, + role_id INTEGER, + PRIMARY KEY (user_id, role_id), + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (role_id) REFERENCES roles(id) +); +``` + +## Exemplos de Permissões + +### Permissões Básicas +- `view_own_data`: Visualizar seus próprios dados +- `edit_own_data`: Editar seus próprios dados +- `view_cell_data`: Visualizar dados da célula + +### Permissões de Célula +- `manage_cell_members`: Gerenciar membros da célula +- `create_cell_member`: Criar novos membros na célula +- `view_cell_reports`: Visualizar relatórios da célula + +### Permissões de Setor +- `manage_sector_cells`: Gerenciar células do setor +- `create_sector_cell`: Criar novas células no setor +- `view_sector_reports`: Visualizar relatórios do setor + +### Permissões de CR +- `manage_cr_sectors`: Gerenciar setores do CR +- `create_cr_sector`: Criar novos setores no CR +- `view_cr_reports`: Visualizar relatórios do CR + +### Permissões de CC +- `manage_cc_crs`: Gerenciar CRs +- `create_cc_cr`: Criar novos CRs +- `view_cc_reports`: Visualizar relatórios nacionais +- `system_config`: Configurar o sistema + +## Uso no Código + +```python +# Verificar permissão +if user.has_permission('manage_cell_members'): + # Permitir ação + +# Verificar nível +if user.has_role_level(3): # Membro de Setor + # Permitir ação + +# Verificar hierarquia +if user.is_higher_or_equal_than(other_user): + # Permitir ação +``` + +# Controle de Acesso Baseado em Funções (RBAC) + +## Estrutura Hierárquica + +O sistema possui uma estrutura hierárquica com os seguintes níveis: +- Célula (base) +- Setor (agrupa células) +- Comitê Regional - CR (agrupa setores) +- Comitê Central - CC (único, agrupa CRs) + +## Regras de Associação + +- Cada militante pertence a apenas uma célula +- Cada célula pertence a apenas um setor +- Cada setor pertence a apenas um CR +- Existe apenas um Comitê Central (CC) + +## Permissões por Instância + +### Célula +- **Secretário(a)**: + - `MANAGE_CELL_MEMBERS`: Gerenciar membros da célula + - `VIEW_CELL_DATA`: Visualizar dados da célula + - `VIEW_CELL_REPORTS`: Visualizar relatórios da célula + - `REGISTER_CELL_PAYMENT`: Registrar pagamentos da célula + +- **Tesoureiro(a)**: + - `VIEW_CELL_DATA`: Visualizar dados da célula + - `VIEW_CELL_REPORTS`: Visualizar relatórios da célula + - `REGISTER_CELL_PAYMENT`: Registrar pagamentos da célula + +- **Militante**: + - `VIEW_OWN_DATA`: Visualizar apenas seus próprios dados + +### Setor +- **Secretário(a)**: + - `MANAGE_SECTOR_CELLS`: Gerenciar células do setor + - `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor + - `REGISTER_SECTOR_PAYMENT`: Registrar pagamentos do setor + +- **Tesoureiro(a)**: + - `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor + - `REGISTER_SECTOR_PAYMENT`: Registrar pagamentos do setor + +### CR +- **Secretário(a)**: + - `MANAGE_CR_SECTORS`: Gerenciar setores do CR + - `VIEW_CR_REPORTS`: Visualizar relatórios do CR + - `REGISTER_CR_PAYMENT`: Registrar pagamentos do CR + +- **Tesoureiro(a)**: + - `VIEW_CR_REPORTS`: Visualizar relatórios do CR + - `REGISTER_CR_PAYMENT`: Registrar pagamentos do CR + +### CC +- **Secretário(a)**: + - `MANAGE_CC_CRS`: Gerenciar CRs + - `VIEW_CC_REPORTS`: Visualizar relatórios do CC + - `REGISTER_CC_PAYMENT`: Registrar pagamentos do CC + - `SYSTEM_CONFIG`: Configurar o sistema + +- **Tesoureiro(a)**: + - `VIEW_CC_REPORTS`: Visualizar relatórios do CC + - `REGISTER_CC_PAYMENT`: Registrar pagamentos do CC + +## Regras de Acesso a Dados + +1. **Visualização de Dados**: + - Militantes podem ver apenas seus próprios dados + - Secretários e tesoureiros podem ver dados de sua instância + - O CC tem acesso a todos os dados + +2. **Registro de Pagamentos**: + - Apenas tesoureiros e secretários podem registrar pagamentos + - O registro é restrito à instância do usuário + - O CC pode registrar pagamentos em qualquer nível + +## Implementação Técnica + +O controle de acesso é implementado através de: + +1. **Decorators**: + - `@require_login`: Verifica se o usuário está logado + - `@require_permission`: Verifica se o usuário tem uma permissão específica + - `@require_instance_permission`: Verifica permissão em uma instância específica + - `@require_instance_access`: Verifica acesso a uma instância específica + +2. **Verificações de Acesso**: + - Cada rota verifica as permissões necessárias + - O acesso é negado se o usuário não tiver as permissões requeridas + - Mensagens de erro são exibidas para o usuário + +3. **Filtragem de Dados**: + - As consultas ao banco de dados são filtradas baseadas nas permissões + - Cada nível hierárquico tem suas próprias regras de acesso \ No newline at end of file diff --git a/functions/base.py b/functions/base.py new file mode 100644 index 0000000..7be06e9 --- /dev/null +++ b/functions/base.py @@ -0,0 +1,21 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +import os + +# Configuração do banco de dados +DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///database.db') +engine = create_engine(DATABASE_URL) +Session = sessionmaker(bind=engine) + +# Base declarativa do SQLAlchemy +Base = declarative_base() + +def get_db_connection(): + """Retorna uma nova sessão do banco de dados""" + session = Session() + try: + return session + except Exception as e: + session.rollback() + raise e \ No newline at end of file diff --git a/functions/database.py b/functions/database.py index a39cb8f..1e90171 100644 --- a/functions/database.py +++ b/functions/database.py @@ -1,39 +1,47 @@ -from sqlalchemy import create_engine, Column, Integer, String, Boolean, Numeric, Date, ForeignKey, DateTime, Text -from sqlalchemy.orm import relationship, sessionmaker -from sqlalchemy.ext.declarative import declarative_base +from datetime import datetime, timedelta from werkzeug.security import generate_password_hash, check_password_hash -import pyotp +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum +from sqlalchemy.orm import sessionmaker, relationship, backref import os +import pyotp 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 +import enum +from flask_login import UserMixin +from .rbac import Role, Permission, role_permissions, user_roles +from .base import Base, engine, Session # Configurar caminho do banco de dados db_dir = Path.home() / '.local' / 'share' / 'controles' db_dir.mkdir(parents=True, exist_ok=True) db_path = db_dir / 'database.db' -# Configurar engine com NullPool -engine = create_engine( - f'sqlite:///{db_path}', - echo=True, - poolclass=NullPool # Usar NullPool ao invés do pool padrão -) - -Base = declarative_base() SessionLocal = sessionmaker(bind=engine) def get_db_connection(): """ - Retorna uma nova sessão do banco de dados SQLite + Retorna uma nova sessão do banco de dados SQLite e verifica timeout """ + session = SessionLocal() try: - return SessionLocal() - finally: - engine.dispose() + # Verificar timeout para usuários logados + usuario_atual = session.query(Usuario).filter( + Usuario.ultimo_login.isnot(None), + Usuario.ultimo_logout.is_(None) + ).first() + + if usuario_atual and usuario_atual.check_session_timeout(): + usuario_atual.logout() + session.commit() + raise Exception("Sessão expirada. Por favor, faça login novamente.") + + return session + except Exception as e: + session.close() + raise e def execute_query(query, params=None): """ @@ -68,6 +76,7 @@ class Celula(Base): secretario_rel = relationship("Militante", foreign_keys=[secretario]) responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas]) pagamentos = relationship("PagamentoCelula", back_populates="celula") + usuarios = relationship("Usuario", back_populates="celula") class ComiteRegional(Base): __tablename__ = 'comites_regionais' @@ -86,6 +95,7 @@ class ComiteRegional(Base): correspondente_jornal_rel = relationship("Militante", foreign_keys=[correspondente_jornal]) setores = relationship("Setor", back_populates="cr") celulas = relationship("Celula", back_populates="cr") + usuarios = relationship("Usuario", back_populates="cr") class EmailMilitante(Base): __tablename__ = 'emails_militantes' @@ -159,6 +169,13 @@ class Militante(Base): otp_secret = Column(String(32)) temp_token = Column(String(64)) temp_token_expiry = Column(DateTime) + # Novo campo para Quadro-Orientador + quadro_orientador = Column(Boolean, default=False) + # Campos para Aspirante + aspirante = Column(Boolean, default=True) # Por padrão, todo novo militante é aspirante + data_inicio_aspirante = Column(DateTime, default=datetime.utcnow) + avaliacao_aspirante = Column(Text) + data_avaliacao_aspirante = Column(DateTime) # Relacionamentos existentes cotas_mensais = relationship("CotaMensal", back_populates="militante") @@ -175,6 +192,10 @@ class Militante(Base): MNS = 8 MPS = 16 JUVENTUDE = 32 + QUADRO_ORIENTADOR = 64 + ASPIRANTE = 128 + RESPONSAVEL_FINANCAS = 256 + RESPONSAVEL_IMPRENSA = 512 @staticmethod def get_responsabilidades_list(): @@ -184,7 +205,11 @@ class Militante(Base): (Militante.IMPRENSA, "Imprensa"), (Militante.MNS, "MNS"), (Militante.MPS, "MPS"), - (Militante.JUVENTUDE, "Juventude") + (Militante.JUVENTUDE, "Juventude"), + (Militante.QUADRO_ORIENTADOR, "Quadro-Orientador"), + (Militante.ASPIRANTE, "Aspirante"), + (Militante.RESPONSAVEL_FINANCAS, "Responsável de Finanças"), + (Militante.RESPONSAVEL_IMPRENSA, "Responsável de Imprensa") ] def set_responsabilidades(self, resp_list): @@ -243,6 +268,26 @@ class Militante(Base): mail.send(msg) + def generate_username(self): + """Gera um nome de usuário único baseado no primeiro nome e um código""" + from sqlalchemy import func + db = get_db_connection() + try: + # Pega o primeiro nome + primeiro_nome = self.nome.split()[0].lower() + + # Conta quantos usuários já existem com esse prefixo + count = db.query(func.count(Usuario.id)).filter( + Usuario.username.like(f"{primeiro_nome}%") + ).scalar() + + # Gera o código (número sequencial) + codigo = str(count + 1).zfill(3) + + return f"{primeiro_nome}{codigo}" + finally: + db.close() + class CotaMensal(Base): __tablename__ = 'cotas_mensais' @@ -375,10 +420,16 @@ class RelatorioVendasMateriais(Base): setor = relationship("Setor", back_populates="relatorios_vendas") comite = relationship("ComiteCentral", back_populates="relatorios_vendas") -class Usuario(Base): +class TipoUsuario(enum.Enum): + ADMIN = "admin" + CR_RESPONSAVEL = "cr_responsavel" + SETOR_RESPONSAVEL = "setor_responsavel" + USUARIO = "usuario" + +class Usuario(Base, UserMixin): __tablename__ = 'usuarios' - id = Column(Integer, primary_key=True, autoincrement=True) + id = Column(Integer, primary_key=True) username = Column(String(50), unique=True, nullable=False) password_hash = Column(String(255), nullable=False) email = Column(String(100), unique=True, nullable=False) @@ -392,74 +443,111 @@ class Usuario(Base): motivo_logout = Column(String(100)) cr_id = Column(Integer, ForeignKey('comites_regionais.id')) celula_id = Column(Integer, ForeignKey('celulas.id')) + session_timeout = Column(Integer, default=30) + tipo = Column(String(17), nullable=False) + ultima_atividade = Column(DateTime, default=datetime.utcnow) + # Relacionamento com militante + militante_id = Column(Integer, ForeignKey('militantes.id')) + militante = relationship("Militante", backref=backref("usuario", uselist=False)) - role = relationship("Role", back_populates="usuarios") - setor = relationship("Setor", back_populates="usuarios") - celula = relationship("Celula") - cr = relationship("ComiteRegional") + # Relacionamentos + roles = relationship("Role", secondary="user_roles", back_populates="users") + setor = relationship('Setor', back_populates='usuarios') + cr = relationship('ComiteRegional', back_populates='usuarios') + celula = relationship('Celula', back_populates='usuarios') - def __init__(self, username, password, is_admin=False): + def get_id(self): + return str(self.id) + + @property + def is_authenticated(self): + return True + + @property + def is_active(self): + return self.ativo + + @property + def is_anonymous(self): + return False + + def __init__(self, username, password, is_admin=False, email=None, tipo="USUARIO"): self.username = username self.password_hash = generate_password_hash(password) self.is_admin = is_admin - self.otp_secret = pyotp.random_base32() # Gerar segredo OTP na criação + self.email = email self.ativo = True + self.session_timeout = 30 + self.tipo = tipo + self.ultima_atividade = datetime.utcnow() def check_password(self, password): return check_password_hash(self.password_hash, password) - def verify_otp(self, otp_code): - """Verifica se o código OTP fornecido é válido""" - if not self.otp_secret: - print(f"Erro: Usuário {self.username} não tem segredo OTP configurado") - return False - - totp = pyotp.TOTP(self.otp_secret) - is_valid = totp.verify(otp_code) - print(f"Verificando OTP para {self.username}") - print(f"Segredo: {self.otp_secret}") - print(f"Código fornecido: {otp_code}") - print(f"Resultado: {'válido' if is_valid else 'inválido'}") - return is_valid + def update_last_activity(self): + self.ultima_atividade = datetime.utcnow() + + def is_session_expired(self): + if not self.ultima_atividade: + return True + time_diff = datetime.utcnow() - self.ultima_atividade + return time_diff.total_seconds() > (self.session_timeout * 60) + + def check_session_timeout(self): + """Verifica se a sessão do usuário expirou""" + if not self.ultima_atividade: + return True + time_diff = datetime.utcnow() - self.ultima_atividade + return time_diff.total_seconds() > (self.session_timeout * 60) + + def has_permission(self, permission_name): + """Verifica se o usuário tem uma determinada permissão""" + for role in self.roles: + for permission in role.permissions: + if permission.nome == permission_name: + return True + return False + + def has_role(self, role_nivel): + """Verifica se o usuário tem um determinado nível de role""" + for role in self.roles: + if role.nivel == role_nivel: + return True + return False def get_otp_uri(self): - """Gera a URI para o QR code do OTP""" + """Gera a URI para autenticação em duas etapas""" if not self.otp_secret: self.otp_secret = pyotp.random_base32() - - totp = pyotp.TOTP(self.otp_secret) - return totp.provisioning_uri( - name=self.username, + return pyotp.totp.TOTP(self.otp_secret).provisioning_uri( + self.username, issuer_name="Sistema de Controles" ) -class Role(Base): - __tablename__ = 'roles' - - id = Column(Integer, primary_key=True, autoincrement=True) - nome = Column(String(50), unique=True, nullable=False) - nivel = Column(Integer, nullable=False) # Nível hierárquico (1: admin, 2: coordenador, 3: militante) - - usuarios = relationship("Usuario", back_populates="role") - permissoes = relationship("RolePermissao", back_populates="role") + def verify_otp(self, code): + """Verifica se um código OTP é válido""" + if not self.otp_secret: + print(f"Erro: OTP secret não configurado para o usuário {self.username}") + return False + + print(f"Verificando OTP para usuário {self.username}") + print(f"OTP Secret: {self.otp_secret}") + print(f"Código fornecido: {code}") + + totp = pyotp.totp.TOTP(self.otp_secret) + is_valid = totp.verify(code) + + print(f"Resultado da verificação: {'Válido' if is_valid else 'Inválido'}") + print(f"Tempo atual: {datetime.utcnow()}") + print(f"Período atual: {totp.timecode(datetime.utcnow())}") + + return is_valid -class Permissao(Base): - __tablename__ = 'permissoes' - - id = Column(Integer, primary_key=True, autoincrement=True) - nome = Column(String(50), unique=True, nullable=False) - descricao = Column(String(255)) - - roles = relationship("RolePermissao", back_populates="permissao") - -class RolePermissao(Base): - __tablename__ = 'roles_permissoes' - - role_id = Column(Integer, ForeignKey('roles.id'), primary_key=True) - permissao_id = Column(Integer, ForeignKey('permissoes.id'), primary_key=True) - - role = relationship("Role", back_populates="permissoes") - permissao = relationship("Permissao", back_populates="roles") + def logout(self): + """Registra o logout do usuário""" + self.ultimo_logout = datetime.utcnow() + self.motivo_logout = "Logout manual" + self.ultima_atividade = None class PagamentoCelula(Base): __tablename__ = 'pagamentos_celula' @@ -537,12 +625,9 @@ class TransacaoPIX(Base): if os.path.exists(db_path): os.remove(db_path) -def init_database(): - """Inicializa o banco de dados com dados básicos""" - print("Inicializando banco de dados...") - - # Criar todas as tabelas - Base.metadata.create_all(engine) +def init_rbac(): + """Inicializa o sistema RBAC""" + print("Inicializando sistema RBAC...") session = SessionLocal() try: @@ -554,7 +639,7 @@ def init_database(): # Criar role de admin admin_role = session.query(Role).filter_by(nome="Administrador").first() if not admin_role: - admin_role = Role(nome="Administrador", nivel=1) + admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL) session.add(admin_role) session.commit() @@ -568,6 +653,11 @@ def init_database(): admin.email = "admin@example.com" admin.role_id = admin_role.id + # Adicionar apenas a permissão de system_config ao admin + permission = session.query(Permission).filter_by(nome='system_config').first() + if permission and permission not in admin_role.permissions: + admin_role.permissions.append(permission) + session.add(admin) session.commit() @@ -578,7 +668,100 @@ def init_database(): print(f"OTP Secret: {admin.otp_secret}") else: print("Usuário admin já existe") + # Garantir que o admin tenha apenas a permissão de system_config + admin_role = session.query(Role).filter_by(nome="Administrador").first() + if admin_role: + # Remover todas as permissões atuais + admin_role.permissions = [] + + # Adicionar apenas a permissão de system_config + permission = session.query(Permission).filter_by(nome='system_config').first() + if permission: + admin_role.permissions.append(permission) + + session.commit() + except Exception as e: + print(f"Erro na inicialização do sistema RBAC: {e}") + session.rollback() + raise + finally: + session.close() + +def init_database(): + """Inicializa o banco de dados com dados básicos""" + print("Inicializando banco de dados...") + + # Criar todas as tabelas + Base.metadata.drop_all(engine) # Remover todas as tabelas existentes + Base.metadata.create_all(engine) + + session = SessionLocal() + try: + # Criar role de administrador + admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL) + session.add(admin_role) + session.commit() + + # Verificar se existe um QR code salvo + qr_path = Path('admin_qr.png') + admin_otp_secret = None + + if qr_path.exists(): + # Extrair o segredo OTP do nome do arquivo temporário dentro do QR + try: + import re + with open('admin_qr.txt', 'r') as f: + qr_content = f.read() + # O segredo OTP está no formato otpauth://totp/admin?secret=XXXXX&issuer=Sistema%20de%20Controles + match = re.search(r'secret=([A-Z0-9]+)&', qr_content) + if match: + admin_otp_secret = match.group(1) + print(f"Usando OTP existente: {admin_otp_secret}") + except Exception as e: + print(f"Erro ao ler OTP existente: {e}") + + if not admin_otp_secret: + admin_otp_secret = pyotp.random_base32() + print(f"Novo OTP gerado: {admin_otp_secret}") + + # Criar usuário admin + admin = Usuario( + username="admin", + password="admin123", + is_admin=True, + email="admin@example.com", + tipo="ADMIN" + ) + admin.role_id = admin_role.id + admin.otp_secret = admin_otp_secret + session.add(admin) + session.commit() + + # Gerar novo QR code se não existir + if not qr_path.exists(): + totp = pyotp.totp.TOTP(admin_otp_secret) + provisioning_uri = totp.provisioning_uri("admin", issuer_name="Sistema de Controles") + + # Salvar a URI em um arquivo texto para referência futura + with open('admin_qr.txt', 'w') as f: + f.write(provisioning_uri) + + # Gerar QR code + import qrcode + qr = qrcode.QRCode(version=1, box_size=10, border=5) + qr.add_data(provisioning_uri) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + img.save('admin_qr.png') + + print("=== Usuário Admin Criado ===") + print(f"Username: admin") + print(f"Senha: admin123") + print(f"Email: {admin.email}") + print(f"OTP Secret: {admin.otp_secret}") + print(f"QR Code: {qr_path}") + except Exception as e: print(f"Erro na inicialização do banco: {e}") session.rollback() @@ -586,6 +769,9 @@ def init_database(): finally: session.close() + # Inicializar o sistema RBAC + init_rbac() + # Inicializar o banco de dados automaticamente quando o módulo for importado init_database() diff --git a/functions/decorators.py b/functions/decorators.py new file mode 100644 index 0000000..69840c1 --- /dev/null +++ b/functions/decorators.py @@ -0,0 +1,197 @@ +from functools import wraps +from flask import session, redirect, url_for, flash +from flask_login import current_user, login_required +from sqlalchemy.orm import joinedload +from .database import get_db_connection, Usuario +from .rbac import Permission + +def require_login(f): + """Decorador para verificar se o usuário está logado""" + @wraps(f) + def decorated_function(*args, **kwargs): + if not current_user.is_authenticated: + flash('Você precisa estar logado para acessar esta página.', 'error') + return redirect(url_for('login')) + + db = get_db_connection() + try: + # Carregar o usuário com suas roles + user = db.query(Usuario).options( + joinedload(Usuario.roles) + ).get(current_user.id) + + if not user: + flash('Usuário não encontrado.', 'error') + return redirect(url_for('login')) + + # Atualiza timestamp da última atividade + user.update_last_activity() + db.commit() + + return f(*args, **kwargs) + finally: + db.close() + return decorated_function + +def require_permission(permission_name): + """Decorador para verificar se o usuário tem uma permissão específica""" + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if not current_user.is_authenticated: + flash('Você precisa estar logado para acessar esta página.', 'error') + return redirect(url_for('login')) + + db = get_db_connection() + try: + user = db.query(Usuario).get(current_user.id) + if not user or not user.has_permission(permission_name): + flash('Você não tem permissão para acessar esta página.', 'error') + return redirect(url_for('index')) + + # Atualiza timestamp da última atividade + user.update_last_activity() + db.commit() + + return f(*args, **kwargs) + finally: + db.close() + return decorated_function + return decorator + +def require_role(role_name): + """Decorador para verificar se o usuário tem um papel específico""" + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if not current_user.is_authenticated: + flash('Você precisa estar logado para acessar esta página.', 'error') + return redirect(url_for('login')) + + db = get_db_connection() + try: + user = db.query(Usuario).get(current_user.id) + if not user or not user.has_role(role_name): + flash('Você não tem permissão para acessar esta página.', 'error') + return redirect(url_for('index')) + + # Atualiza timestamp da última atividade + user.update_last_activity() + db.commit() + + return f(*args, **kwargs) + finally: + db.close() + return decorated_function + return decorator + +def require_minimum_role(min_level): + """Decorador para verificar se o usuário tem um papel com nível mínimo""" + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if not current_user.is_authenticated: + flash('Você precisa estar logado para acessar esta página.', 'error') + return redirect(url_for('login')) + + db = get_db_connection() + try: + user = db.query(Usuario).get(current_user.id) + if not user: + flash('Usuário não encontrado.', 'error') + return redirect(url_for('login')) + + highest_role = user.get_highest_role() + if not highest_role or highest_role.nivel < min_level: + flash('Você não tem permissão para acessar esta página.', 'error') + return redirect(url_for('index')) + + # Atualiza timestamp da última atividade + user.update_last_activity() + db.commit() + + return f(*args, **kwargs) + finally: + db.close() + return decorated_function + return decorator + +def require_instance_permission(permission_name): + """Decorator para verificar se o usuário tem permissão em uma instância específica""" + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_id' not in session: + flash('Você precisa estar logado para acessar esta página.', 'error') + return redirect(url_for('login')) + + db = get_db_connection() + try: + user = db.query(Usuario).get(session['user_id']) + if not user: + flash('Usuário não encontrado.', 'error') + return redirect(url_for('login')) + + # Verificar se o usuário tem a permissão em alguma instância + if not (user.has_permission(permission_name) or + user.has_permission(f"{permission_name}_sector") or + user.has_permission(f"{permission_name}_cr") or + user.has_permission(f"{permission_name}_cc")): + flash('Você não tem permissão para acessar esta página.', 'error') + return redirect(url_for('index')) + + # Atualiza timestamp da última atividade + user.update_last_activity() + db.commit() + + return f(*args, **kwargs) + finally: + db.close() + return decorated_function + return decorator + +def require_instance_access(instance_type, instance_id): + """Decorator para verificar se o usuário tem acesso a uma instância específica""" + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_id' not in session: + flash('Você precisa estar logado para acessar esta página.', 'error') + return redirect(url_for('login')) + + db = get_db_connection() + try: + user = db.query(Usuario).get(session['user_id']) + if not user: + flash('Usuário não encontrado.', 'error') + return redirect(url_for('login')) + + # Verificar acesso baseado na instância do usuário + if instance_type == 'celula': + if not (user.celula_id == instance_id or + user.has_permission(Permission.VIEW_SECTOR_REPORTS) or + user.has_permission(Permission.VIEW_CR_REPORTS) or + user.has_permission(Permission.VIEW_CC_REPORTS)): + flash('Você não tem acesso a esta célula.', 'error') + return redirect(url_for('index')) + elif instance_type == 'setor': + if not (user.setor_id == instance_id or + user.has_permission(Permission.VIEW_CR_REPORTS) or + user.has_permission(Permission.VIEW_CC_REPORTS)): + flash('Você não tem acesso a este setor.', 'error') + return redirect(url_for('index')) + elif instance_type == 'cr': + if not (user.cr_id == instance_id or + user.has_permission(Permission.VIEW_CC_REPORTS)): + flash('Você não tem acesso a este CR.', 'error') + return redirect(url_for('index')) + + # Atualiza timestamp da última atividade + user.update_last_activity() + db.commit() + + return f(*args, **kwargs) + finally: + db.close() + return decorated_function + return decorator \ No newline at end of file diff --git a/functions/permissions.py b/functions/permissions.py new file mode 100644 index 0000000..f0314ad --- /dev/null +++ b/functions/permissions.py @@ -0,0 +1,222 @@ +from functools import wraps +from flask import abort, g +from .database import Militante, Celula, Setor, CR, CC + +def check_permission(permission_func): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if not permission_func(*args, **kwargs): + abort(403) + return f(*args, **kwargs) + return decorated_function + return decorator + +def can_manage_militante(militante_id): + """Verifica se o usuário atual pode gerenciar um militante específico.""" + if not g.user or not g.user.militante: + return False + + militante = Militante.query.get(militante_id) + if not militante: + return False + + # Secretário Geral e Secretário de Organização podem gerenciar qualquer militante + if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO): + return True + + # Secretário de CC pode gerenciar militantes do seu CC + if g.user.militante.responsabilidades & Militante.SECRETARIO_CC: + if militante.celula.setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id: + return True + + # Secretário de CR pode gerenciar militantes do seu CR + if g.user.militante.responsabilidades & Militante.SECRETARIO_CR: + if militante.celula.setor.cr_id == g.user.militante.celula.setor.cr_id: + return True + + # Secretário de Setor pode gerenciar militantes do seu setor + if g.user.militante.responsabilidades & Militante.SECRETARIO_SETOR: + if militante.celula.setor_id == g.user.militante.celula.setor_id: + return True + + # Secretário de Célula pode gerenciar militantes da sua célula + if g.user.militante.responsabilidades & Militante.SECRETARIO_CELULA: + if militante.celula_id == g.user.militante.celula_id: + return True + + return False + +def can_manage_celula(celula_id): + """Verifica se o usuário atual pode gerenciar uma célula específica.""" + if not g.user or not g.user.militante: + return False + + celula = Celula.query.get(celula_id) + if not celula: + return False + + # Secretário Geral e Secretário de Organização podem gerenciar qualquer célula + if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO): + return True + + # Secretário de CC pode gerenciar células do seu CC + if g.user.militante.responsabilidades & Militante.SECRETARIO_CC: + if celula.setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id: + return True + + # Secretário de CR pode gerenciar células do seu CR + if g.user.militante.responsabilidades & Militante.SECRETARIO_CR: + if celula.setor.cr_id == g.user.militante.celula.setor.cr_id: + return True + + # Secretário de Setor pode gerenciar células do seu setor + if g.user.militante.responsabilidades & Militante.SECRETARIO_SETOR: + if celula.setor_id == g.user.militante.celula.setor_id: + return True + + return False + +def can_manage_setor(setor_id): + """Verifica se o usuário atual pode gerenciar um setor específico.""" + if not g.user or not g.user.militante: + return False + + setor = Setor.query.get(setor_id) + if not setor: + return False + + # Secretário Geral e Secretário de Organização podem gerenciar qualquer setor + if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO): + return True + + # Secretário de CC pode gerenciar setores do seu CC + if g.user.militante.responsabilidades & Militante.SECRETARIO_CC: + if setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id: + return True + + # Secretário de CR pode gerenciar setores do seu CR + if g.user.militante.responsabilidades & Militante.SECRETARIO_CR: + if setor.cr_id == g.user.militante.celula.setor.cr_id: + return True + + return False + +def can_manage_cr(cr_id): + """Verifica se o usuário atual pode gerenciar um CR específico.""" + if not g.user or not g.user.militante: + return False + + cr = CR.query.get(cr_id) + if not cr: + return False + + # Secretário Geral e Secretário de Organização podem gerenciar qualquer CR + if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO): + return True + + # Secretário de CC pode gerenciar CRs do seu CC + if g.user.militante.responsabilidades & Militante.SECRETARIO_CC: + if cr.cc_id == g.user.militante.celula.setor.cr.cc_id: + return True + + return False + +def can_manage_cc(cc_id): + """Verifica se o usuário atual pode gerenciar um CC específico.""" + if not g.user or not g.user.militante: + return False + + # Apenas Secretário Geral e Secretário de Organização podem gerenciar CCs + if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO): + return True + + return False + +def can_manage_financas(instancia_id, tipo_instancia): + """Verifica se o usuário atual pode gerenciar finanças de uma instância específica.""" + if not g.user or not g.user.militante: + return False + + # Secretário Geral e Secretário de Organização podem gerenciar finanças de qualquer instância + if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO): + return True + + # Responsável de Finanças da instância pode gerenciar suas finanças + if tipo_instancia == 'celula': + celula = Celula.query.get(instancia_id) + if celula and celula.responsavel_financas_id == g.user.militante.id: + return True + elif tipo_instancia == 'setor': + setor = Setor.query.get(instancia_id) + if setor and setor.responsavel_financas_id == g.user.militante.id: + return True + elif tipo_instancia == 'cr': + cr = CR.query.get(instancia_id) + if cr and cr.responsavel_financas_id == g.user.militante.id: + return True + elif tipo_instancia == 'cc': + cc = CC.query.get(instancia_id) + if cc and cc.responsavel_financas_id == g.user.militante.id: + return True + + return False + +def can_manage_imprensa(instancia_id, tipo_instancia): + """Verifica se o usuário atual pode gerenciar imprensa de uma instância específica.""" + if not g.user or not g.user.militante: + return False + + # Secretário Geral e Secretário de Organização podem gerenciar imprensa de qualquer instância + if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO): + return True + + # Responsável de Imprensa da instância pode gerenciar sua imprensa + if tipo_instancia == 'celula': + celula = Celula.query.get(instancia_id) + if celula and celula.responsavel_imprensa_id == g.user.militante.id: + return True + elif tipo_instancia == 'setor': + setor = Setor.query.get(instancia_id) + if setor and setor.responsavel_imprensa_id == g.user.militante.id: + return True + elif tipo_instancia == 'cr': + cr = CR.query.get(instancia_id) + if cr and cr.responsavel_imprensa_id == g.user.militante.id: + return True + elif tipo_instancia == 'cc': + cc = CC.query.get(instancia_id) + if cc and cc.responsavel_imprensa_id == g.user.militante.id: + return True + + return False + +def can_manage_responsabilidades(militante_id): + """Verifica se o usuário atual pode gerenciar responsabilidades de um militante específico.""" + if not g.user or not g.user.militante: + return False + + militante = Militante.query.get(militante_id) + if not militante: + return False + + # Secretário Geral e Secretário de Organização podem gerenciar responsabilidades de qualquer militante + if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO): + return True + + # Secretário de CC pode gerenciar responsabilidades de militantes do seu CC + if g.user.militante.responsabilidades & Militante.SECRETARIO_CC: + if militante.celula.setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id: + return True + + # Secretário de CR pode gerenciar responsabilidades de militantes do seu CR + if g.user.militante.responsabilidades & Militante.SECRETARIO_CR: + if militante.celula.setor.cr_id == g.user.militante.celula.setor.cr_id: + return True + + # Secretário de Setor pode gerenciar responsabilidades de militantes do seu setor + if g.user.militante.responsabilidades & Militante.SECRETARIO_SETOR: + if militante.celula.setor_id == g.user.militante.celula.setor_id: + return True + + return False \ No newline at end of file diff --git a/functions/rbac.py b/functions/rbac.py new file mode 100644 index 0000000..6984e40 --- /dev/null +++ b/functions/rbac.py @@ -0,0 +1,292 @@ +from sqlalchemy import Column, Integer, String, Text, ForeignKey, Table +from sqlalchemy.orm import relationship +from .base import Base + +# Tabela de mapeamento Role-Permission +role_permissions = Table( + 'role_permissions', + Base.metadata, + Column('role_id', Integer, ForeignKey('roles.id'), primary_key=True), + Column('permission_id', Integer, ForeignKey('permissions.id'), primary_key=True) +) + +# Tabela de mapeamento User-Role +user_roles = Table( + 'user_roles', + Base.metadata, + Column('user_id', Integer, ForeignKey('usuarios.id'), primary_key=True), + Column('role_id', Integer, ForeignKey('roles.id'), primary_key=True) +) + +class Role(Base): + __tablename__ = 'roles' + + id = Column(Integer, primary_key=True, autoincrement=True) + nome = Column(String(50), unique=True, nullable=False) + nivel = Column(Integer, nullable=False) # Nível hierárquico + descricao = Column(Text) + + # Relacionamentos + permissions = relationship("Permission", secondary=role_permissions, back_populates="roles") + users = relationship("Usuario", secondary=user_roles, back_populates="roles") + + # Níveis de role + MILITANTE_BASICO = 1 + SECRETARIO_CELULA = 2 + MEMBRO_SETOR = 3 + SECRETARIO_SETOR = 4 + MEMBRO_CR = 5 + SECRETARIO_CR = 6 + MEMBRO_CC = 7 + SECRETARIO_GERAL = 8 + + @staticmethod + def get_roles_list(): + return [ + (Role.MILITANTE_BASICO, "Militante Básico"), + (Role.SECRETARIO_CELULA, "Secretário de Célula"), + (Role.MEMBRO_SETOR, "Membro de Setor"), + (Role.SECRETARIO_SETOR, "Secretário de Setor"), + (Role.MEMBRO_CR, "Membro de CR"), + (Role.SECRETARIO_CR, "Secretário de CR"), + (Role.MEMBRO_CC, "Membro do CC"), + (Role.SECRETARIO_GERAL, "Secretário Geral") + ] + +class Permission(Base): + __tablename__ = 'permissions' + + id = Column(Integer, primary_key=True, autoincrement=True) + nome = Column(String(50), unique=True, nullable=False) + descricao = Column(Text) + + # Relacionamentos + roles = relationship("Role", secondary=role_permissions, back_populates="permissions") + + # Permissões básicas + VIEW_OWN_DATA = "view_own_data" + EDIT_OWN_DATA = "edit_own_data" + VIEW_CELL_DATA = "view_cell_data" + CREATE_MILITANT = "create_militant" # Nova permissão para criar militantes + + # Permissões de célula + MANAGE_CELL_MEMBERS = "manage_cell_members" + CREATE_CELL_MEMBER = "create_cell_member" + VIEW_CELL_REPORTS = "view_cell_reports" + REGISTER_CELL_PAYMENT = "register_cell_payment" + + # Permissões de setor + MANAGE_SECTOR_CELLS = "manage_sector_cells" + CREATE_SECTOR_CELL = "create_sector_cell" + VIEW_SECTOR_REPORTS = "view_sector_reports" + REGISTER_SECTOR_PAYMENT = "register_sector_payment" + + # Permissões de CR + MANAGE_CR_SECTORS = "manage_cr_sectors" + CREATE_CR_SECTOR = "create_cr_sector" + VIEW_CR_REPORTS = "view_cr_reports" + REGISTER_CR_PAYMENT = "register_cr_payment" + + # Permissões de CC + MANAGE_CC_CRS = "manage_cc_crs" + CREATE_CC_CR = "create_cc_cr" + VIEW_CC_REPORTS = "view_cc_reports" + REGISTER_CC_PAYMENT = "register_cc_payment" + SYSTEM_CONFIG = "system_config" + + @staticmethod + def get_permissions_list(): + return [ + # Permissões básicas + (Permission.VIEW_OWN_DATA, "Visualizar próprios dados"), + (Permission.EDIT_OWN_DATA, "Editar próprios dados"), + (Permission.VIEW_CELL_DATA, "Visualizar dados da célula"), + (Permission.CREATE_MILITANT, "Criar novos militantes"), # Nova permissão + + # Permissões de célula + (Permission.MANAGE_CELL_MEMBERS, "Gerenciar membros da célula"), + (Permission.CREATE_CELL_MEMBER, "Criar membros na célula"), + (Permission.VIEW_CELL_REPORTS, "Visualizar relatórios da célula"), + (Permission.REGISTER_CELL_PAYMENT, "Registrar pagamentos da célula"), + + # Permissões de setor + (Permission.MANAGE_SECTOR_CELLS, "Gerenciar células do setor"), + (Permission.CREATE_SECTOR_CELL, "Criar células no setor"), + (Permission.VIEW_SECTOR_REPORTS, "Visualizar relatórios do setor"), + (Permission.REGISTER_SECTOR_PAYMENT, "Registrar pagamentos do setor"), + + # Permissões de CR + (Permission.MANAGE_CR_SECTORS, "Gerenciar setores do CR"), + (Permission.CREATE_CR_SECTOR, "Criar setores no CR"), + (Permission.VIEW_CR_REPORTS, "Visualizar relatórios do CR"), + (Permission.REGISTER_CR_PAYMENT, "Registrar pagamentos do CR"), + + # Permissões de CC + (Permission.MANAGE_CC_CRS, "Gerenciar CRs"), + (Permission.CREATE_CC_CR, "Criar CRs"), + (Permission.VIEW_CC_REPORTS, "Visualizar relatórios nacionais"), + (Permission.REGISTER_CC_PAYMENT, "Registrar pagamentos nacionais"), + (Permission.SYSTEM_CONFIG, "Configurar sistema") + ] + +def init_rbac(): + """Inicializa o sistema RBAC com roles e permissões básicas""" + from .database import get_db_connection + session = get_db_connection() + + try: + # Criar roles se não existirem + for nivel, nome in Role.get_roles_list(): + role = session.query(Role).filter_by(nivel=nivel).first() + if not role: + role = Role(nome=nome, nivel=nivel) + session.add(role) + + # Criar permissões se não existirem + for nome, descricao in Permission.get_permissions_list(): + permission = session.query(Permission).filter_by(nome=nome).first() + if not permission: + permission = Permission(nome=nome, descricao=descricao) + session.add(permission) + + session.commit() + + # Mapear permissões para roles + for role in session.query(Role).all(): + # Militante Básico + if role.nivel == Role.MILITANTE_BASICO: + role.permissions = [ + session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(), + session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first() + ] + + # Secretário de Célula + elif role.nivel == Role.SECRETARIO_CELULA: + role.permissions = [ + session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(), + session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(), + session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(), + session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.REGISTER_CELL_PAYMENT).first() + ] + + # Membro de Setor + elif role.nivel == Role.MEMBRO_SETOR: + role.permissions = [ + session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(), + session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(), + session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(), + session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first() + ] + + # Secretário de Setor + elif role.nivel == Role.SECRETARIO_SETOR: + role.permissions = [ + session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(), + session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(), + session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(), + session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(), + session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(), + session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first() + ] + + # Membro de CR + elif role.nivel == Role.MEMBRO_CR: + role.permissions = [ + session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(), + session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(), + session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(), + session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(), + session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first() + ] + + # Secretário de CR + elif role.nivel == Role.SECRETARIO_CR: + role.permissions = [ + session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(), + session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(), + session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(), + session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(), + session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(), + session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(), + session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first() + ] + + # Membro do CC + elif role.nivel == Role.MEMBRO_CC: + role.permissions = [ + session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(), + session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(), + session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(), + session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(), + session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(), + session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first() + ] + + # Secretário Geral + elif role.nivel == Role.SECRETARIO_GERAL: + role.permissions = [ + session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(), + session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(), + session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(), + session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(), + session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(), + session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(), + session.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(), + session.query(Permission).filter_by(nome=Permission.MANAGE_CC_CRS).first(), + session.query(Permission).filter_by(nome=Permission.CREATE_CC_CR).first(), + session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first(), + session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first() + ] + + # Administrador + elif role.nome == "Administrador": + role.permissions = [ + session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first() + ] + + session.commit() + + except Exception as e: + print(f"Erro ao inicializar RBAC: {e}") + session.rollback() + raise + finally: + session.close() \ No newline at end of file diff --git a/init_system.py b/init_system.py new file mode 100644 index 0000000..7827c30 --- /dev/null +++ b/init_system.py @@ -0,0 +1,58 @@ +from create_admin import create_admin +from create_test_users import create_test_users +from functions.database import get_db_connection, Usuario +from functions.rbac import Role + +def init_system(): + print("=== Inicializando Sistema ===") + + # Criar admin + print("\nCriando usuário admin...") + create_admin() + + # Criar usuários de teste + print("\nCriando usuários de teste...") + create_test_users() + + # Verificar configuração + print("\n=== Verificando Configuração ===") + session = get_db_connection() + try: + # Verificar admin + admin = session.query(Usuario).filter_by(username='admin').first() + if admin: + print("Admin: OK") + print(f"OTP configurado: {'Sim' if admin.otp_secret else 'Não'}") + else: + print("Admin: FALHOU") + + # Verificar usuários de teste + test_users = ['aligner', 'tester', 'deployer'] + for username in test_users: + user = session.query(Usuario).filter_by(username=username).first() + if user: + print(f"{username}: OK") + print(f"OTP configurado: {'Sim' if user.otp_secret else 'Não'}") + else: + print(f"{username}: FALHOU") + + print("\n=== Instruções ===") + print("1. Use o aplicativo autenticador para configurar o OTP de cada usuário") + print("2. Faça login com cada usuário para testar") + print("3. Altere a senha no primeiro login") + print("\nCredenciais:") + print("Admin:") + print(" Usuário: admin") + print(" Senha: admin123") + print("\nUsuários de teste:") + print(" Usuário: aligner, tester, deployer") + print(" Senha: Test123!@#") + + except Exception as e: + print(f"Erro ao verificar configuração: {str(e)}") + session.rollback() + finally: + session.close() + +if __name__ == "__main__": + init_system() \ No newline at end of file diff --git a/migrations/versions/add_responsaveis_financas_imprensa.py b/migrations/versions/add_responsaveis_financas_imprensa.py new file mode 100644 index 0000000..2e2d29d --- /dev/null +++ b/migrations/versions/add_responsaveis_financas_imprensa.py @@ -0,0 +1,64 @@ +"""add_responsaveis_financas_imprensa + +Revision ID: add_responsaveis_financas_imprensa +Revises: add_aspirante_fields +Create Date: 2024-03-19 10:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'add_responsaveis_financas_imprensa' +down_revision = 'add_aspirante_fields' +branch_labels = None +depends_on = None + + +def upgrade(): + # Adicionar colunas na tabela celulas + op.add_column('celulas', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True)) + op.add_column('celulas', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True)) + op.create_foreign_key('fk_celulas_responsavel_financas', 'celulas', 'militantes', ['responsavel_financas_id'], ['id']) + op.create_foreign_key('fk_celulas_responsavel_imprensa', 'celulas', 'militantes', ['responsavel_imprensa_id'], ['id']) + + # Adicionar colunas na tabela setores + op.add_column('setores', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True)) + op.add_column('setores', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True)) + op.create_foreign_key('fk_setores_responsavel_financas', 'setores', 'militantes', ['responsavel_financas_id'], ['id']) + op.create_foreign_key('fk_setores_responsavel_imprensa', 'setores', 'militantes', ['responsavel_imprensa_id'], ['id']) + + # Adicionar colunas na tabela crs + op.add_column('crs', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True)) + op.add_column('crs', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True)) + op.create_foreign_key('fk_crs_responsavel_financas', 'crs', 'militantes', ['responsavel_financas_id'], ['id']) + op.create_foreign_key('fk_crs_responsavel_imprensa', 'crs', 'militantes', ['responsavel_imprensa_id'], ['id']) + + # Adicionar colunas na tabela ccs + op.add_column('ccs', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True)) + op.add_column('ccs', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True)) + op.create_foreign_key('fk_ccs_responsavel_financas', 'ccs', 'militantes', ['responsavel_financas_id'], ['id']) + op.create_foreign_key('fk_ccs_responsavel_imprensa', 'ccs', 'militantes', ['responsavel_imprensa_id'], ['id']) + + +def downgrade(): + # Remover foreign keys + op.drop_constraint('fk_celulas_responsavel_financas', 'celulas', type_='foreignkey') + op.drop_constraint('fk_celulas_responsavel_imprensa', 'celulas', type_='foreignkey') + op.drop_constraint('fk_setores_responsavel_financas', 'setores', type_='foreignkey') + op.drop_constraint('fk_setores_responsavel_imprensa', 'setores', type_='foreignkey') + op.drop_constraint('fk_crs_responsavel_financas', 'crs', type_='foreignkey') + op.drop_constraint('fk_crs_responsavel_imprensa', 'crs', type_='foreignkey') + op.drop_constraint('fk_ccs_responsavel_financas', 'ccs', type_='foreignkey') + op.drop_constraint('fk_ccs_responsavel_imprensa', 'ccs', type_='foreignkey') + + # Remover colunas + op.drop_column('celulas', 'responsavel_financas_id') + op.drop_column('celulas', 'responsavel_imprensa_id') + op.drop_column('setores', 'responsavel_financas_id') + op.drop_column('setores', 'responsavel_imprensa_id') + op.drop_column('crs', 'responsavel_financas_id') + op.drop_column('crs', 'responsavel_imprensa_id') + op.drop_column('ccs', 'responsavel_financas_id') + op.drop_column('ccs', 'responsavel_imprensa_id') \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2986ff8..21c5927 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,16 @@ Flask==3.0.2 Flask-SQLAlchemy==3.1.1 -SQLAlchemy==2.0.39 +Flask-Login==0.6.3 +Flask-WTF==1.2.1 +Flask-Mail==0.9.1 +SQLAlchemy==2.0.27 Werkzeug==3.0.1 +python-dotenv==1.0.1 pyotp==2.9.0 qrcode==7.4.2 -pillow==11.0.0 -python-dotenv==1.0.1 -flask-login==0.6.3 -flask-wtf==1.2.1 +Pillow==10.2.0 email-validator==2.1.0.post1 -Bootstrap-Flask==2.4.1 +cryptography==42.0.2 +bcrypt==4.1.2 +Bootstrap-Flask==2.3.3 flask-bootstrap5==0.1.dev1 -flask-mail diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5b99be3 --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +from setuptools import setup, find_packages + +setup( + name="controles", + version="0.1.0", + packages=find_packages(), + install_requires=[ + "fastapi", + "uvicorn", + "sqlalchemy", + "python-jose[cryptography]", + "passlib[bcrypt]", + "python-multipart", + "qrcode", + "pillow", + "python-dotenv" + ], +) \ No newline at end of file diff --git a/sql/migrate_db.py b/sql/migrate_db.py new file mode 100644 index 0000000..17081d9 --- /dev/null +++ b/sql/migrate_db.py @@ -0,0 +1,66 @@ +import os +import sqlite3 +import sys +from pathlib import Path + +# Adiciona o diretório raiz ao PYTHONPATH +root_dir = str(Path(__file__).parent.parent) +sys.path.append(root_dir) + +from functions.base import Base, engine +from functions.database import init_database +from functions.rbac import init_rbac + +def execute_sql_file(file_path): + """Executa um arquivo SQL""" + print(f"Executando arquivo {file_path}...") + + try: + with open(file_path, 'r') as sql_file: + sql_commands = sql_file.read().split(';') + + conn = sqlite3.connect('database.db') + cursor = conn.cursor() + + for command in sql_commands: + command = command.strip() + if command: + try: + cursor.execute(command) + except sqlite3.OperationalError as e: + if "already exists" in str(e): + print(f"Aviso: {str(e)}") + else: + raise e + + conn.commit() + conn.close() + print(f"Arquivo {file_path} executado com sucesso!") + except Exception as e: + print(f"Erro ao executar {file_path}: {str(e)}") + raise e + +def migrate_database(): + """Executa a migração do banco de dados""" + print("Inicializando banco de dados...") + + # Criar todas as tabelas + Base.metadata.create_all(engine) + + # Executar scripts SQL + sql_dir = Path(__file__).parent + rbac_tables_sql = sql_dir / 'rbac_tables.sql' + + if rbac_tables_sql.exists(): + execute_sql_file(rbac_tables_sql) + + # Inicializar RBAC + init_rbac() + + # Inicializar banco de dados + init_database() + + print("Migração concluída com sucesso!") + +if __name__ == '__main__': + migrate_database() \ No newline at end of file diff --git a/sql/migrate_rbac.py b/sql/migrate_rbac.py new file mode 100644 index 0000000..05b854f --- /dev/null +++ b/sql/migrate_rbac.py @@ -0,0 +1,47 @@ +from functions.database import get_db_connection, Usuario +from functions.rbac import Role, Permission + +def migrate_existing_users(): + """Migra os usuários existentes para o novo sistema RBAC""" + session = get_db_connection() + + try: + # Buscar todos os usuários + usuarios = session.query(Usuario).all() + + # Buscar ou criar role de administrador + admin_role = session.query(Role).filter_by(nome="Administrador").first() + if not admin_role: + admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL) + session.add(admin_role) + + # Buscar ou criar role de militante básico + militante_role = session.query(Role).filter_by(nome="Militante Básico").first() + if not militante_role: + militante_role = Role(nome="Militante Básico", nivel=Role.MILITANTE_BASICO) + session.add(militante_role) + + # Atualizar usuários + for usuario in usuarios: + # Se o usuário já tem roles, pular + if usuario.roles: + continue + + # Atribuir role com base no is_admin + if usuario.is_admin: + usuario.roles.append(admin_role) + else: + usuario.roles.append(militante_role) + + session.commit() + print("Migração de usuários concluída com sucesso!") + + except Exception as e: + session.rollback() + print(f"Erro durante a migração de usuários: {str(e)}") + raise e + finally: + session.close() + +if __name__ == '__main__': + migrate_existing_users() \ No newline at end of file diff --git a/sql/rbac_tables.sql b/sql/rbac_tables.sql new file mode 100644 index 0000000..3a4f8a6 --- /dev/null +++ b/sql/rbac_tables.sql @@ -0,0 +1,152 @@ +-- Tabela de roles +CREATE TABLE IF NOT EXISTS roles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nome VARCHAR(50) NOT NULL UNIQUE, + nivel INTEGER NOT NULL, + descricao TEXT +); + +-- Tabela de permissões +CREATE TABLE IF NOT EXISTS permissions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nome VARCHAR(50) NOT NULL UNIQUE, + descricao TEXT +); + +-- Tabela de mapeamento Role-Permission +CREATE TABLE IF NOT EXISTS role_permissions ( + role_id INTEGER NOT NULL, + permission_id INTEGER NOT NULL, + PRIMARY KEY (role_id, permission_id), + FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, + FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE +); + +-- Tabela de mapeamento User-Role +CREATE TABLE IF NOT EXISTS user_roles ( + user_id INTEGER NOT NULL, + role_id INTEGER NOT NULL, + PRIMARY KEY (user_id, role_id), + FOREIGN KEY (user_id) REFERENCES usuarios(id) ON DELETE CASCADE, + FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE +); + +-- Inserir roles básicas +INSERT OR IGNORE INTO roles (nome, nivel, descricao) VALUES + ('Militante Básico', 1, 'Militante com permissões básicas'), + ('Secretário de Célula', 2, 'Responsável por uma célula'), + ('Membro de Setor', 3, 'Membro de um setor'), + ('Secretário de Setor', 4, 'Responsável por um setor'), + ('Membro de CR', 5, 'Membro de um Comitê Regional'), + ('Secretário de CR', 6, 'Responsável por um Comitê Regional'), + ('Membro do CC', 7, 'Membro do Comitê Central'), + ('Secretário Geral', 8, 'Secretário Geral ou de Organização do CC'); + +-- Inserir permissões básicas +INSERT OR IGNORE INTO permissions (nome, descricao) VALUES + -- Permissões básicas + ('view_own_data', 'Visualizar próprios dados'), + ('edit_own_data', 'Editar próprios dados'), + ('view_cell_data', 'Visualizar dados da célula'), + ('create_militant', 'Criar novos militantes'), + + -- Permissões de célula + ('manage_cell_members', 'Gerenciar membros da célula'), + ('create_cell_member', 'Criar membros na célula'), + ('view_cell_reports', 'Visualizar relatórios da célula'), + + -- Permissões de setor + ('manage_sector_cells', 'Gerenciar células do setor'), + ('create_sector_cell', 'Criar células no setor'), + ('view_sector_reports', 'Visualizar relatórios do setor'), + + -- Permissões de CR + ('manage_cr_sectors', 'Gerenciar setores do CR'), + ('create_cr_sector', 'Criar setores no CR'), + ('view_cr_reports', 'Visualizar relatórios do CR'), + + -- Permissões de CC + ('manage_cc_crs', 'Gerenciar CRs'), + ('create_cc_cr', 'Criar CRs'), + ('view_cc_reports', 'Visualizar relatórios nacionais'), + ('system_config', 'Configurar sistema'); + +-- Mapear permissões para roles +-- Militante Básico +INSERT OR IGNORE INTO role_permissions (role_id, permission_id) +SELECT r.id, p.id +FROM roles r, permissions p +WHERE r.nome = 'Militante Básico' +AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data'); + +-- Secretário de Célula +INSERT OR IGNORE INTO role_permissions (role_id, permission_id) +SELECT r.id, p.id +FROM roles r, permissions p +WHERE r.nome = 'Secretário de Célula' +AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data', + 'manage_cell_members', 'create_cell_member', 'view_cell_reports', + 'create_militant'); + +-- Membro de Setor +INSERT OR IGNORE INTO role_permissions (role_id, permission_id) +SELECT r.id, p.id +FROM roles r, permissions p +WHERE r.nome = 'Membro de Setor' +AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data', + 'manage_cell_members', 'create_cell_member', 'view_cell_reports', + 'view_sector_reports', 'create_militant'); + +-- Secretário de Setor +INSERT OR IGNORE INTO role_permissions (role_id, permission_id) +SELECT r.id, p.id +FROM roles r, permissions p +WHERE r.nome = 'Secretário de Setor' +AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data', + 'manage_cell_members', 'create_cell_member', 'view_cell_reports', + 'view_sector_reports', 'manage_sector_cells', 'create_sector_cell', + 'create_militant'); + +-- Membro de CR +INSERT OR IGNORE INTO role_permissions (role_id, permission_id) +SELECT r.id, p.id +FROM roles r, permissions p +WHERE r.nome = 'Membro de CR' +AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data', + 'manage_cell_members', 'create_cell_member', 'view_cell_reports', + 'view_sector_reports', 'manage_sector_cells', 'create_sector_cell', + 'view_cr_reports', 'create_militant'); + +-- Secretário de CR +INSERT OR IGNORE INTO role_permissions (role_id, permission_id) +SELECT r.id, p.id +FROM roles r, permissions p +WHERE r.nome = 'Secretário de CR' +AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data', + 'manage_cell_members', 'create_cell_member', 'view_cell_reports', + 'view_sector_reports', 'manage_sector_cells', 'create_sector_cell', + 'view_cr_reports', 'manage_cr_sectors', 'create_cr_sector', + 'create_militant'); + +-- Membro do CC +INSERT OR IGNORE INTO role_permissions (role_id, permission_id) +SELECT r.id, p.id +FROM roles r, permissions p +WHERE r.nome = 'Membro do CC' +AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data', + 'manage_cell_members', 'create_cell_member', 'view_cell_reports', + 'view_sector_reports', 'manage_sector_cells', 'create_sector_cell', + 'view_cr_reports', 'manage_cr_sectors', 'create_cr_sector', + 'view_cc_reports', 'create_militant'); + +-- Secretário Geral +INSERT OR IGNORE INTO role_permissions (role_id, permission_id) +SELECT r.id, p.id +FROM roles r, permissions p +WHERE r.nome = 'Secretário Geral' +AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data', + 'manage_cell_members', 'create_cell_member', 'view_cell_reports', + 'view_sector_reports', 'manage_sector_cells', 'create_sector_cell', + 'view_cr_reports', 'manage_cr_sectors', 'create_cr_sector', + 'view_cc_reports', 'manage_cc_crs', 'create_cc_cr', + 'system_config', 'create_militant'); \ No newline at end of file diff --git a/templates/alterar_senha.html b/templates/alterar_senha.html new file mode 100644 index 0000000..db2f04a --- /dev/null +++ b/templates/alterar_senha.html @@ -0,0 +1,51 @@ +{% extends 'base.html' %} + +{% block title %}Alterar Senha{% endblock %} + +{% block content %} +
+
+
+
+
+

Alterar Senha

+
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+ + +
+ +
+ + + + A senha deve ter no mínimo 8 caracteres e conter letras e números. + +
+ +
+ + +
+ +
+ + Cancelar +
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 9cbafde..ab18881 100644 --- a/templates/base.html +++ b/templates/base.html @@ -65,42 +65,60 @@ @@ -111,5 +129,28 @@ + \ No newline at end of file diff --git a/templates/criar_instancia.html b/templates/criar_instancia.html new file mode 100644 index 0000000..e15c31d --- /dev/null +++ b/templates/criar_instancia.html @@ -0,0 +1,111 @@ +{% extends 'base.html' %} + +{% block title %}Criar {{ tipo_instancia }}{% endblock %} + +{% block content %} +
+
+
+

Criar {{ tipo_instancia }}

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+
+ + +
+ Por favor, insira o nome da {{ tipo_instancia }}. +
+
+ + {% if tipo_instancia != 'Célula' %} +
+ + +
+ Por favor, selecione uma {{ instancia_superior }}. +
+
+ {% endif %} +
+ +
+
+ + +
+ Por favor, selecione o responsável geral. +
+
+ +
+ + +
+
+ +
+
+ + +
+
+ +
+ + Cancelar +
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/criar_militante.html b/templates/criar_militante.html new file mode 100644 index 0000000..7ec3827 --- /dev/null +++ b/templates/criar_militante.html @@ -0,0 +1,106 @@ +{% extends 'base.html' %} + +{% block title %}Criar Militante{% endblock %} + +{% block content %} +
+
+
+

Criar Militante

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+
+ + +
+ Por favor, insira o nome do militante. +
+
+ +
+ + +
+ Por favor, insira um email válido. +
+
+
+ +
+
+ + +
+ Por favor, selecione uma célula. +
+
+
+ +
+
+ +
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + Cancelar +
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..e6cb2c7 --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,284 @@ +{% extends 'base.html' %} + +{% block title %}Dashboard Administrativo{% endblock %} + +{% block content %} +
+

Dashboard Administrativo

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+
Gerenciamento de Acessos
+
+
+
+ + + + + + + + + + + + + + + {% for user in users %} + {% if current_user.has_permission('system_config') or + (current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or + (current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) or + (current_user.has_permission('manage_cell_members') and user.celula_id == current_user.celula_id) %} + + + + + + + + + + + {% endif %} + {% endfor %} + +
IDUsuárioEmailStatusÚltimo LoginNívelResponsabilidadesAções
{{ user.id }}{{ user.username }}{{ user.email }} + {% if user.ativo %} + Ativo + {% else %} + Inativo + {% endif %} + {{ user.ultimo_login.strftime('%d/%m/%Y %H:%M') if user.ultimo_login else 'Nunca' }} + {{ user.role }} + {% if current_user.has_permission('system_config') or + (current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or + (current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) %} + + {% endif %} + + {% if user.militante %} + {% if user.militante.quadro_orientador %} + Quadro-Orientador + {% endif %} + {% if user.militante.aspirante %} + Aspirante + + (desde {{ user.militante.data_inicio_aspirante.strftime('%d/%m/%Y') }}) + + {% if user.militante.avaliacao_aspirante %} + + {% endif %} + {% endif %} + {% if current_user.has_permission('system_config') or + (current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or + (current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) %} + {% if user.militante.quadro_orientador %} + + {% else %} + + {% endif %} + {% if user.militante.aspirante %} + {% if datetime.utcnow() - user.militante.data_inicio_aspirante >= timedelta(days=90) %} + {% if not user.militante.avaliacao_aspirante %} + + {% endif %} + + {% endif %} + {% else %} + + {% endif %} + {% endif %} + {% endif %} + +
+ {% if current_user.has_permission('system_config') or + (current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or + (current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) or + (current_user.has_permission('manage_cell_members') and user.celula_id == current_user.celula_id) %} + + + + {% endif %} +
+
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/dashboard_admin.html b/templates/dashboard_admin.html new file mode 100644 index 0000000..d9c539a --- /dev/null +++ b/templates/dashboard_admin.html @@ -0,0 +1,83 @@ +{% extends 'base.html' %} + +{% block title %}Dashboard Administrativo{% endblock %} + +{% block content %} +
+

Dashboard Administrativo

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+
Gerenciamento de Usuários
+
+
+
+ + + + + + + + + + + + + {% for usuario in usuarios %} + + + + + + + + + {% endfor %} + +
IDUsuárioEmailAdminOTP ConfiguradoAções
{{ usuario.id }}{{ usuario.username }}{{ usuario.email }} + {% if usuario.is_admin %} + Sim + {% else %} + Não + {% endif %} + + {% if usuario.otp_secret %} + Sim + {% else %} + Não + {% endif %} + +
+ +
+
+
+
+
+ +
+
+
Ações Rápidas
+
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/editar_instancia.html b/templates/editar_instancia.html new file mode 100644 index 0000000..0853dd2 --- /dev/null +++ b/templates/editar_instancia.html @@ -0,0 +1,111 @@ +{% extends 'base.html' %} + +{% block title %}Editar {{ tipo_instancia }}{% endblock %} + +{% block content %} +
+
+
+

Editar {{ tipo_instancia }}

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+
+ + +
+ Por favor, insira o nome da {{ tipo_instancia }}. +
+
+ + {% if tipo_instancia != 'Célula' %} +
+ + +
+ Por favor, selecione uma {{ instancia_superior }}. +
+
+ {% endif %} +
+ +
+
+ + +
+ Por favor, selecione o responsável geral. +
+
+ +
+ + +
+
+ +
+
+ + +
+
+ +
+ + Cancelar +
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/editar_militante.html b/templates/editar_militante.html index 34b9fd8..a98451a 100644 --- a/templates/editar_militante.html +++ b/templates/editar_militante.html @@ -27,99 +27,107 @@
- - + +
- Por favor, insira o CPF do militante. + Por favor, insira um email válido.
+
+ + +
+ Por favor, insira o CPF do militante. +
+
+
- +
+ +
-
- -
+
- +
+ +
-
- -
+
- +
+ +
-
- -
+
- +
+ +
-
- -
+
- +
+ +
-
- -
+
- +
+ +
-
- -
+
- +
+ +
-
- -
+
@@ -128,14 +136,14 @@
- +
+ +
-
- -
+
@@ -163,6 +173,30 @@
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+
+
Cancelar diff --git a/templates/home.html b/templates/home.html index 17bb28e..ab9e401 100644 --- a/templates/home.html +++ b/templates/home.html @@ -17,82 +17,16 @@ {% endwith %}
+ {% for link in links %}
-
-
Militantes
-
-

Gerencie os militantes da organização.

- Listar Militantes - Novo Militante -
-
-
- -
-
-
-
Pagamentos
-
-
-

Gerencie os pagamentos dos militantes.

- Listar Pagamentos - Novo Pagamento -
-
-
- -
-
-
-
Materiais
-
-
-

Gerencie os materiais da organização.

- Listar Materiais - Novo Material -
-
-
- -
-
-
-
Vendas
-
-
-

Gerencie as vendas de materiais.

- Listar Vendas - Nova Venda -
-
-
- -
-
-
-
Relatórios
-
-
-

Gerencie os relatórios da organização.

- Relatórios de Cotas - Relatórios de Vendas -
-
-
- -
-
-
-
Configurações
-
-
-

Gerencie as configurações da organização.

- Novo Usuário +
{{ link.text }}
+ Acessar
+ {% endfor %}
diff --git a/templates/listar_instancias.html b/templates/listar_instancias.html new file mode 100644 index 0000000..dfda41d --- /dev/null +++ b/templates/listar_instancias.html @@ -0,0 +1,79 @@ +{% extends 'base.html' %} + +{% block title %}Lista de {{ tipo_instancia }}s{% endblock %} + +{% block content %} +
+
+
+

Lista de {{ tipo_instancia }}s

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + + + +
+ + + + + {% if tipo_instancia != 'Célula' %} + + {% endif %} + + + + + + + + {% for instancia in instancias %} + + + {% if tipo_instancia != 'Célula' %} + + {% endif %} + + + + + + {% endfor %} + +
Nome{{ instancia_superior }}Responsável GeralResponsável de FinançasResponsável de ImprensaAções
{{ instancia.nome }}{{ instancia.instancia_superior.nome }}{{ instancia.responsavel_geral.nome }} + {% if instancia.responsavel_financas %} + {{ instancia.responsavel_financas.nome }} + {% else %} + - + {% endif %} + + {% if instancia.responsavel_imprensa %} + {{ instancia.responsavel_imprensa.nome }} + {% else %} + - + {% endif %} + + Editar + +
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/listar_militantes.html b/templates/listar_militantes.html index 262434c..f512908 100644 --- a/templates/listar_militantes.html +++ b/templates/listar_militantes.html @@ -1,6 +1,6 @@ {% extends 'base.html' %} -{% block title %}Listar Militantes{% endblock %} +{% block title %}Lista de Militantes{% endblock %} {% block content %}
@@ -17,67 +17,42 @@ {% endwith %}
- +
- - - - - - - - - - - - - - - - - - - + + {% for militante in militantes %} - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + {% endfor %}
ID NomeCPFTítulo EleitoralData de NascimentoData de EntradaData de EfetivaçãoTelefone 1Telefone 2ProfissãoRegime de TrabalhoEmpresaContratanteInstituição de EnsinoTipo de InstituiçãoSindicatoCargo SindicalDirigente SindicalCentral SindicalSetorEmail CélulaResponsabilidades Ações
{{ militante.id }}{{ militante.nome }}{{ militante.cpf }}{{ militante.titulo_eleitoral }}{{ militante.data_nascimento.strftime('%d/%m/%Y') }}{{ militante.data_entrada_oci.strftime('%d/%m/%Y') }}{{ militante.data_efetivacao_oci.strftime('%d/%m/%Y') }}{{ militante.telefone1 }}{{ militante.telefone2 }}{{ militante.profissao }}{{ militante.regime_trabalho }}{{ militante.empresa }}{{ militante.contratante }}{{ militante.instituicao_ensino }}{{ militante.tipo_instituicao }}{{ militante.sindicato }}{{ militante.cargo_sindical }}{{ 'Sim' if militante.dirigente_sindical else 'Não' }}{{ militante.central_sindical }}{{ militante.setor.nome }}{{ militante.celula.nome }} - Editar - Excluir -
{{ militante.nome }}{{ militante.email }}{{ militante.celula.nome }} + {% if militante.responsabilidades & Militante.RESPONSAVEL_FINANCAS %} + Finanças + {% endif %} + {% if militante.responsabilidades & Militante.RESPONSAVEL_IMPRENSA %} + Imprensa + {% endif %} + {% if militante.responsabilidades & Militante.QUADRO_ORIENTADOR %} + Quadro-Orientador + {% endif %} + + Editar + +
@@ -85,4 +60,12 @@
+ + {% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html index 3a40097..5be53bc 100644 --- a/templates/login.html +++ b/templates/login.html @@ -4,43 +4,38 @@ {% block content %}
-
-
-
+
+
+
-

Login

+

Login

{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} -
- {{ message }} - {% if category == 'danger' %} -
- Se o problema persistir, contate o administrador. - {% endif %} -
+
{{ message }}
{% endfor %} {% endif %} {% endwith %} - -
+ +
- +
+
+
- - - Digite o código de 6 dígitos do seu aplicativo autenticador + + + Digite o código gerado pelo seu aplicativo autenticador
+
diff --git a/templates/novo_militante.html b/templates/novo_militante.html index e2cc335..def4809 100644 --- a/templates/novo_militante.html +++ b/templates/novo_militante.html @@ -22,6 +22,12 @@
+
+ + + Este email será usado para login e comunicação do sistema +
+
@@ -115,25 +121,67 @@
- - + + {% for cr in crs %} + {% endfor %}
- - + + {% for setor in setores %} + {% endfor %}
+
+ + {% if pode_criar_celula %} +
+
+ + +
+
+ + +
+
+ {% endif %} + +
+ +
+ + {% if pode_criar_celula %} + + {% endif %} +
+
Voltar @@ -143,5 +191,66 @@
+ + {% endblock %} diff --git a/templates/novo_usuario.html b/templates/novo_usuario.html deleted file mode 100644 index ddda868..0000000 --- a/templates/novo_usuario.html +++ /dev/null @@ -1,74 +0,0 @@ -{% extends 'base.html' %} - -{% block title %}Novo Usuário{% endblock %} - -{% block content %} -
-
-
-

Novo Usuário

- - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
{{ message }}
- {% endfor %} - {% endif %} - {% endwith %} - - -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - Voltar - Início -
- -
-
-
-{% endblock %} \ No newline at end of file diff --git a/tests/test_permissions.py b/tests/test_permissions.py new file mode 100644 index 0000000..ad5d4c0 --- /dev/null +++ b/tests/test_permissions.py @@ -0,0 +1,205 @@ +import unittest +from flask import Flask, g +from functions.database import db, Militante, Celula, Setor, CR, CC +from functions.permissions import ( + can_manage_militante, can_manage_celula, can_manage_setor, can_manage_cr, can_manage_cc, + can_manage_financas, can_manage_imprensa, can_manage_responsabilidades +) + +class TestPermissions(unittest.TestCase): + def setUp(self): + self.app = Flask(__name__) + self.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' + self.app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + db.init_app(self.app) + + with self.app.app_context(): + db.create_all() + + # Criar instâncias + self.cc = CC(nome='CC 1') + db.session.add(self.cc) + + self.cr = CR(nome='CR 1', cc=self.cc) + db.session.add(self.cr) + + self.setor = Setor(nome='Setor 1', cr=self.cr) + db.session.add(self.setor) + + self.celula = Celula(nome='Célula 1', setor=self.setor) + db.session.add(self.celula) + + # Criar militantes com diferentes responsabilidades + self.militante_basico = Militante( + nome='Militante Básico', + email='basico@example.com', + celula=self.celula, + responsabilidades=Militante.MILITANTE_BASICO + ) + db.session.add(self.militante_basico) + + self.secretario_celula = Militante( + nome='Secretário de Célula', + email='celula@example.com', + celula=self.celula, + responsabilidades=Militante.SECRETARIO_CELULA + ) + db.session.add(self.secretario_celula) + + self.secretario_setor = Militante( + nome='Secretário de Setor', + email='setor@example.com', + celula=self.celula, + responsabilidades=Militante.SECRETARIO_SETOR + ) + db.session.add(self.secretario_setor) + + self.secretario_cr = Militante( + nome='Secretário de CR', + email='cr@example.com', + celula=self.celula, + responsabilidades=Militante.SECRETARIO_CR + ) + db.session.add(self.secretario_cr) + + self.secretario_cc = Militante( + nome='Secretário de CC', + email='cc@example.com', + celula=self.celula, + responsabilidades=Militante.SECRETARIO_CC + ) + db.session.add(self.secretario_cc) + + self.secretario_geral = Militante( + nome='Secretário Geral', + email='geral@example.com', + celula=self.celula, + responsabilidades=Militante.SECRETARIO_GERAL + ) + db.session.add(self.secretario_geral) + + self.responsavel_financas = Militante( + nome='Responsável de Finanças', + email='financas@example.com', + celula=self.celula, + responsabilidades=Militante.RESPONSAVEL_FINANCAS + ) + db.session.add(self.responsavel_financas) + + self.responsavel_imprensa = Militante( + nome='Responsável de Imprensa', + email='imprensa@example.com', + celula=self.celula, + responsabilidades=Militante.RESPONSAVEL_IMPRENSA + ) + db.session.add(self.responsavel_imprensa) + + # Atribuir responsáveis às instâncias + self.celula.responsavel_geral = self.secretario_celula + self.celula.responsavel_financas = self.responsavel_financas + self.celula.responsavel_imprensa = self.responsavel_imprensa + + self.setor.responsavel_geral = self.secretario_setor + self.setor.responsavel_financas = self.responsavel_financas + self.setor.responsavel_imprensa = self.responsavel_imprensa + + self.cr.responsavel_geral = self.secretario_cr + self.cr.responsavel_financas = self.responsavel_financas + self.cr.responsavel_imprensa = self.responsavel_imprensa + + self.cc.responsavel_geral = self.secretario_cc + self.cc.responsavel_financas = self.responsavel_financas + self.cc.responsavel_imprensa = self.responsavel_imprensa + + db.session.commit() + + def test_can_manage_militante(self): + with self.app.app_context(): + # Militante básico não pode gerenciar outros militantes + g.user = type('User', (), {'militante': self.militante_basico}) + self.assertFalse(can_manage_militante(self.militante_basico.id)) + + # Secretário de célula pode gerenciar militantes da sua célula + g.user = type('User', (), {'militante': self.secretario_celula}) + self.assertTrue(can_manage_militante(self.militante_basico.id)) + + # Secretário de setor pode gerenciar militantes do seu setor + g.user = type('User', (), {'militante': self.secretario_setor}) + self.assertTrue(can_manage_militante(self.militante_basico.id)) + + # Secretário de CR pode gerenciar militantes do seu CR + g.user = type('User', (), {'militante': self.secretario_cr}) + self.assertTrue(can_manage_militante(self.militante_basico.id)) + + # Secretário de CC pode gerenciar militantes do seu CC + g.user = type('User', (), {'militante': self.secretario_cc}) + self.assertTrue(can_manage_militante(self.militante_basico.id)) + + # Secretário Geral pode gerenciar qualquer militante + g.user = type('User', (), {'militante': self.secretario_geral}) + self.assertTrue(can_manage_militante(self.militante_basico.id)) + + def test_can_manage_financas(self): + with self.app.app_context(): + # Militante básico não pode gerenciar finanças + g.user = type('User', (), {'militante': self.militante_basico}) + self.assertFalse(can_manage_financas(self.celula.id, 'celula')) + + # Responsável de finanças pode gerenciar finanças da sua instância + g.user = type('User', (), {'militante': self.responsavel_financas}) + self.assertTrue(can_manage_financas(self.celula.id, 'celula')) + self.assertTrue(can_manage_financas(self.setor.id, 'setor')) + self.assertTrue(can_manage_financas(self.cr.id, 'cr')) + self.assertTrue(can_manage_financas(self.cc.id, 'cc')) + + # Secretário Geral pode gerenciar finanças de qualquer instância + g.user = type('User', (), {'militante': self.secretario_geral}) + self.assertTrue(can_manage_financas(self.celula.id, 'celula')) + self.assertTrue(can_manage_financas(self.setor.id, 'setor')) + self.assertTrue(can_manage_financas(self.cr.id, 'cr')) + self.assertTrue(can_manage_financas(self.cc.id, 'cc')) + + def test_can_manage_imprensa(self): + with self.app.app_context(): + # Militante básico não pode gerenciar imprensa + g.user = type('User', (), {'militante': self.militante_basico}) + self.assertFalse(can_manage_imprensa(self.celula.id, 'celula')) + + # Responsável de imprensa pode gerenciar imprensa da sua instância + g.user = type('User', (), {'militante': self.responsavel_imprensa}) + self.assertTrue(can_manage_imprensa(self.celula.id, 'celula')) + self.assertTrue(can_manage_imprensa(self.setor.id, 'setor')) + self.assertTrue(can_manage_imprensa(self.cr.id, 'cr')) + self.assertTrue(can_manage_imprensa(self.cc.id, 'cc')) + + # Secretário Geral pode gerenciar imprensa de qualquer instância + g.user = type('User', (), {'militante': self.secretario_geral}) + self.assertTrue(can_manage_imprensa(self.celula.id, 'celula')) + self.assertTrue(can_manage_imprensa(self.setor.id, 'setor')) + self.assertTrue(can_manage_imprensa(self.cr.id, 'cr')) + self.assertTrue(can_manage_imprensa(self.cc.id, 'cc')) + + def test_can_manage_responsabilidades(self): + with self.app.app_context(): + # Militante básico não pode gerenciar responsabilidades + g.user = type('User', (), {'militante': self.militante_basico}) + self.assertFalse(can_manage_responsabilidades(self.militante_basico.id)) + + # Secretário de setor pode gerenciar responsabilidades do seu setor + g.user = type('User', (), {'militante': self.secretario_setor}) + self.assertTrue(can_manage_responsabilidades(self.militante_basico.id)) + + # Secretário de CR pode gerenciar responsabilidades do seu CR + g.user = type('User', (), {'militante': self.secretario_cr}) + self.assertTrue(can_manage_responsabilidades(self.militante_basico.id)) + + # Secretário de CC pode gerenciar responsabilidades do seu CC + g.user = type('User', (), {'militante': self.secretario_cc}) + self.assertTrue(can_manage_responsabilidades(self.militante_basico.id)) + + # Secretário Geral pode gerenciar responsabilidades de qualquer militante + g.user = type('User', (), {'militante': self.secretario_geral}) + self.assertTrue(can_manage_responsabilidades(self.militante_basico.id)) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file