From 0f32eae5cf9b13961e3b3a6b75c17ef31e21c7aa Mon Sep 17 00:00:00 2001 From: andersonid Date: Tue, 15 Apr 2025 14:26:02 -0300 Subject: [PATCH] =?UTF-8?q?refactor(#11):=20Integra=20listagem=20de=20usu?= =?UTF-8?q?=C3=A1rios=20no=20dashboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 134 +---------------------------- pytest.ini | 5 ++ routes/admin.py | 60 ++++++++----- run_tests.sh | 17 ++++ setup.py | 18 ++++ templates/admin/base.html | 4 +- templates/admin/dashboard.html | 152 ++++++++++++++++++++------------- templates/admin/users.html | 108 ----------------------- templates/base.html | 2 +- tests/conftest.py | 33 +++++++ tests/requirements-test.txt | 4 + tests/test_admin_routes.py | 100 ++++++++++++++++++++++ 12 files changed, 316 insertions(+), 321 deletions(-) create mode 100644 pytest.ini create mode 100755 run_tests.sh create mode 100644 setup.py delete mode 100644 templates/admin/users.html create mode 100644 tests/conftest.py create mode 100644 tests/requirements-test.txt create mode 100644 tests/test_admin_routes.py diff --git a/app.py b/app.py index 47a4d63..85ac6ab 100644 --- a/app.py +++ b/app.py @@ -251,7 +251,9 @@ def create_app(): session['is_admin'] = user.is_admin print(f"Login realizado: user_id={user.id}, username={user.username}, is_admin={user.is_admin}") - # Redirecionar para home + # Redirecionar para admin.dashboard se for admin, senão para home + if user.is_admin: + return redirect(url_for("admin.dashboard")) return redirect(url_for("home")) finally: db.close() @@ -1649,136 +1651,6 @@ def create_app(): finally: db.close() - @app.route('/dashboard_admin') - @login_required - def dashboard_admin(): - """Rota para o dashboard administrativo""" - if not current_user.is_admin: - flash('Você não tem permissão para acessar esta página.', 'danger') - return redirect(url_for('home')) - - db = get_db_connection() - try: - # Busca usuários - usuarios = db.query(Usuario).all() - - usuarios_data = [] - for usuario in usuarios: - user_data = { - 'id': usuario.id, - 'username': usuario.username, - 'email': usuario.email, - 'nome': usuario.username, # Usar username como fallback - 'ativo': usuario.ativo, - 'is_admin': usuario.is_admin, - 'last_login': usuario.ultimo_login.strftime('%d/%m/%Y %H:%M') if usuario.ultimo_login else 'Nunca', - 'nivel': 'Administrador' if usuario.is_admin else 'Usuário' - } - usuarios_data.append(user_data) - - return render_template( - 'dashboard_admin.html', - usuarios=usuarios_data - ) - except Exception as e: - import traceback - print(f"Erro no dashboard_admin: {traceback.format_exc()}") - flash('Erro ao carregar dados dos usuários. Por favor, tente novamente.', 'danger') - return redirect(url_for('home')) - finally: - db.close() - - @app.route('/reset_otp/', methods=['POST']) - @login_required - def reset_otp(user_id): - if not current_user.is_admin: - return jsonify({ - 'success': False, - 'error': 'Você não tem permissão para resetar OTP.' - }), 403 - - db = get_db_connection() - try: - usuario = db.query(Usuario).get(user_id) - if not usuario: - return jsonify({ - 'success': False, - 'error': 'Usuário não encontrado.' - }), 404 - - usuario.otp_secret = pyotp.random_base32() - db.commit() - - return jsonify({ - 'success': True - }) - except Exception as e: - db.rollback() - return jsonify({ - 'success': False, - 'error': str(e) - }), 500 - finally: - db.close() - - @app.route('/reset_password/', methods=['POST']) - @login_required - def reset_password(user_id): - if not current_user.is_admin: - return jsonify({ - 'success': False, - 'error': 'Você não tem permissão para resetar senhas.' - }), 403 - - db = get_db_connection() - try: - usuario = db.query(Usuario).get(user_id) - if not usuario: - return jsonify({ - 'success': False, - 'error': 'Usuário não encontrado.' - }), 404 - - nova_senha = ''.join(random.choices(string.ascii_letters + string.digits, k=12)) - usuario.set_password(nova_senha) - - try: - msg = Message( - 'Nova Senha - Sistema de Controles', - recipients=[usuario.email] - ) - msg.body = f'''Olá {usuario.nome}, - -Sua senha foi resetada por um administrador. Sua nova senha é: - -{nova_senha} - -Por favor, altere esta senha no seu próximo login. - -Atenciosamente, -Sistema de Controles''' - - mail.send(msg) - except Exception as e: - print(f"Erro ao enviar email: {str(e)}") - return jsonify({ - 'success': False, - 'error': 'Erro ao enviar email com a nova senha.' - }), 500 - - db.commit() - return jsonify({ - 'success': True - }) - except Exception as e: - db.rollback() - return jsonify({ - 'success': False, - 'error': str(e) - }), 500 - finally: - db.close() - return app def init_system(): diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..69c7848 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +pythonpath = . +testpaths = tests +python_files = test_*.py +addopts = -v --cov=. --cov-report=term-missing \ No newline at end of file diff --git a/routes/admin.py b/routes/admin.py index f8fda8a..f9631db 100644 --- a/routes/admin.py +++ b/routes/admin.py @@ -6,14 +6,28 @@ from sqlalchemy.orm import joinedload import pyotp from werkzeug.security import generate_password_hash import secrets +from functools import wraps +from sqlalchemy.exc import SQLAlchemyError +import logging + +logger = logging.getLogger(__name__) admin_bp = Blueprint('admin', __name__, url_prefix='/admin') +def admin_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if not current_user.is_admin: + flash('Acesso não autorizado.', 'danger') + return redirect(url_for('main.index')) + return f(*args, **kwargs) + return decorated_function + @admin_bp.route('/') @login_required -@require_role('ADMIN') +@admin_required def dashboard(): - """Dashboard principal da área administrativa""" + """Dashboard principal da área administrativa com lista de usuários""" db = get_db_connection() try: # Carregar estatísticas relevantes @@ -21,27 +35,27 @@ def dashboard(): active_users = db.query(Usuario).filter(Usuario.is_active == True).count() inactive_users = total_users - active_users - return render_template( - 'admin/dashboard.html', - total_users=total_users, - active_users=active_users, - inactive_users=inactive_users - ) - finally: - db.close() - -@admin_bp.route('/users') -@login_required -@require_role('ADMIN') -def list_users(): - """Lista todos os usuários do sistema""" - db = get_db_connection() - try: + # Carregar lista de usuários users = db.query(Usuario).options( joinedload(Usuario.roles), joinedload(Usuario.militante) ).all() - return render_template('admin/users.html', users=users) + + return render_template( + 'admin/dashboard.html', + total_users=total_users, + active_users=active_users, + inactive_users=inactive_users, + users=users + ) + except SQLAlchemyError as e: + logger.error(f"Erro ao buscar dados do dashboard: {str(e)}") + flash('Erro ao carregar dados. Por favor, tente novamente.', 'danger') + return render_template('admin/dashboard.html', + total_users=0, + active_users=0, + inactive_users=0, + users=[]) finally: db.close() @@ -55,14 +69,14 @@ def reset_user_otp(user_id): user = db.query(Usuario).get(user_id) if not user: flash('Usuário não encontrado.', 'danger') - return redirect(url_for('admin.list_users')) + return redirect(url_for('admin.dashboard')) # Gerar novo segredo OTP user.otp_secret = pyotp.random_base32() db.commit() flash(f'OTP resetado com sucesso para {user.email}.', 'success') - return redirect(url_for('admin.list_users')) + return redirect(url_for('admin.dashboard')) finally: db.close() @@ -76,7 +90,7 @@ def reset_user_password(user_id): user = db.query(Usuario).get(user_id) if not user: flash('Usuário não encontrado.', 'danger') - return redirect(url_for('admin.list_users')) + return redirect(url_for('admin.dashboard')) # Gerar nova senha aleatória new_password = secrets.token_urlsafe(8) @@ -84,7 +98,7 @@ def reset_user_password(user_id): db.commit() flash(f'Senha resetada com sucesso. Nova senha: {new_password}', 'success') - return redirect(url_for('admin.list_users')) + return redirect(url_for('admin.dashboard')) finally: db.close() diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..ec4df49 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Criar e ativar ambiente virtual +python -m venv venv +source venv/bin/activate + +# Instalar dependências de teste +pip install -r tests/requirements-test.txt + +# Instalar o projeto em modo de desenvolvimento +pip install -e . + +# Executar testes +python -m pytest + +# Desativar ambiente virtual +deactivate \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..546c895 --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +from setuptools import setup, find_packages + +setup( + name="controles", + version="0.1", + packages=find_packages(), + include_package_data=True, + install_requires=[ + 'flask', + 'flask-login', + 'flask-sqlalchemy', + 'flask-wtf', + 'flask-mail', + 'python-dotenv', + 'pyotp', + 'qrcode', + ], +) \ No newline at end of file diff --git a/templates/admin/base.html b/templates/admin/base.html index 045cf14..5d4e3d5 100644 --- a/templates/admin/base.html +++ b/templates/admin/base.html @@ -97,4 +97,6 @@ main { } } -{% endblock %} \ No newline at end of file +{% endblock %} + +{% block extra_scripts %}{% endblock %} \ No newline at end of file diff --git a/templates/admin/dashboard.html b/templates/admin/dashboard.html index fa305e4..9931243 100644 --- a/templates/admin/dashboard.html +++ b/templates/admin/dashboard.html @@ -1,79 +1,117 @@ {% extends "admin/base.html" %} -{% block admin_title %}Dashboard Administrativo{% endblock %} +{% block title %}Dashboard Administrativo{% endblock %} -{% block admin_content %} -
- -
-
+{% block content %} +
+
+
-
-
-
- Total de Usuários
-
{{ total_users }}
-
-
- -
-
+
Total de Usuários
+

{{ total_users }}

+
- - -
-
+
+
-
-
-
- Usuários Ativos
-
{{ active_users }}
-
-
- -
-
+
Usuários Ativos
+

{{ active_users }}

+
- - -
-
+
+
-
-
-
- Usuários Inativos
-
{{ inactive_users }}
-
-
- -
-
+
Usuários Inativos
+

{{ inactive_users }}

+
- -
-
-
-
-
Ações Rápidas
-
- +
+
+
Gerenciamento de Usuários
+
+
+
+ + + + + + + + + + + + {% for user in users %} + + + + + + + + {% endfor %} + +
EmailNomeStatusÚltimo LoginAções
{{ user.email }}{{ user.name }} + + {{ "Ativo" if user.is_active else "Inativo" }} + + {{ user.last_login.strftime('%d/%m/%Y %H:%M') if user.last_login else 'Nunca' }} +
+ + +
+
+ + +
+ +
+{% endblock %} +{% block extra_js %} + {% endblock %} \ No newline at end of file diff --git a/templates/admin/users.html b/templates/admin/users.html deleted file mode 100644 index c3c6096..0000000 --- a/templates/admin/users.html +++ /dev/null @@ -1,108 +0,0 @@ -{% extends "admin/base.html" %} - -{% block admin_title %}Gerenciamento de Usuários{% endblock %} - -{% block admin_content %} -
-
-
Lista de Usuários
-
-
-
- - - - - - - - - - - - {% for user in users %} - - - - - - - - {% endfor %} - -
EmailNomeStatusÚltimo LoginAções
{{ user.email }}{{ user.militante.nome if user.militante else 'N/A' }} - - {{ 'Ativo' if user.is_active else 'Inativo' }} - - {{ user.ultimo_login.strftime('%d/%m/%Y %H:%M') if user.ultimo_login else 'Nunca' }} -
- -
- - -
- - -
- - -
- - - -
-
-
-
-
-{% endblock %} - -{% block extra_js %} - -{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index c938f5b..778505d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -600,7 +600,7 @@
  • - + Administração
  • diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..ffbd6c3 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,33 @@ +import pytest +from app import create_app +from functions.database import init_database, get_db_connection + +@pytest.fixture +def app(): + """Cria uma instância do app para testes""" + app = create_app() + app.config['TESTING'] = True + app.config['WTF_CSRF_ENABLED'] = False + + # Inicializar banco de dados de teste + init_database() + + yield app + + # Limpar banco após os testes + db = get_db_connection() + try: + db.execute('DROP TABLE IF EXISTS usuarios CASCADE') + db.commit() + finally: + db.close() + +@pytest.fixture +def client(app): + """Cria um cliente de teste""" + return app.test_client() + +@pytest.fixture +def runner(app): + """Cria um runner de CLI para testes""" + return app.test_cli_runner() \ No newline at end of file diff --git a/tests/requirements-test.txt b/tests/requirements-test.txt new file mode 100644 index 0000000..af2c56c --- /dev/null +++ b/tests/requirements-test.txt @@ -0,0 +1,4 @@ +pytest==7.4.3 +pytest-cov==4.1.0 +pytest-flask==1.3.0 +coverage==7.3.2 \ No newline at end of file diff --git a/tests/test_admin_routes.py b/tests/test_admin_routes.py new file mode 100644 index 0000000..5e897aa --- /dev/null +++ b/tests/test_admin_routes.py @@ -0,0 +1,100 @@ +import pytest +from flask import url_for +from functions.database import Usuario, get_db_connection +from werkzeug.security import generate_password_hash +import json + +@pytest.fixture +def admin_user(client): + """Fixture que cria um usuário admin para testes""" + db = get_db_connection() + try: + admin = Usuario( + username='admin_test', + email='admin@test.com', + password_hash=generate_password_hash('admin123'), + is_admin=True, + is_active=True + ) + db.add(admin) + db.commit() + return admin + finally: + db.close() + +@pytest.fixture +def auth_admin_client(client, admin_user): + """Fixture que retorna um cliente autenticado como admin""" + client.post('/login', data={ + 'email': 'admin@test.com', + 'password': 'admin123' + }) + return client + +def test_dashboard_access_sem_login(client): + """Testa acesso ao dashboard sem login""" + response = client.get('/admin/') + assert response.status_code == 302 + assert '/login' in response.headers['Location'] + +def test_dashboard_access_com_login(auth_admin_client): + """Testa acesso ao dashboard com login de admin""" + response = auth_admin_client.get('/admin/') + assert response.status_code == 200 + assert b'Dashboard Administrativo' in response.data + +def test_lista_usuarios(auth_admin_client): + """Testa listagem de usuários""" + response = auth_admin_client.get('/admin/users') + assert response.status_code == 200 + assert b'Lista de' in response.data + assert b'admin_test' in response.data + +def test_reset_otp(auth_admin_client, admin_user): + """Testa reset de OTP""" + response = auth_admin_client.post(f'/admin/users/{admin_user.id}/reset-otp') + assert response.status_code == 302 + assert 'success' in response.headers['Location'] + +def test_reset_password(auth_admin_client, admin_user): + """Testa reset de senha""" + response = auth_admin_client.post(f'/admin/users/{admin_user.id}/reset-password') + assert response.status_code == 302 + assert 'success' in response.headers['Location'] + +def test_toggle_status(auth_admin_client, admin_user): + """Testa alteração de status do usuário""" + response = auth_admin_client.post( + f'/admin/users/{admin_user.id}/toggle-status', + headers={'Content-Type': 'application/json'} + ) + data = json.loads(response.data) + assert response.status_code == 200 + assert data['success'] is True + +def test_acesso_nao_admin(client): + """Testa acesso de usuário não admin""" + db = get_db_connection() + try: + # Criar usuário normal + user = Usuario( + username='normal_user', + email='user@test.com', + password_hash=generate_password_hash('user123'), + is_admin=False, + is_active=True + ) + db.add(user) + db.commit() + + # Login + client.post('/login', data={ + 'email': 'user@test.com', + 'password': 'user123' + }) + + # Tentar acessar área admin + response = client.get('/admin/') + assert response.status_code == 403 + finally: + db.close() \ No newline at end of file