From e43b089155c26d73d9f4a05d216ea2bdf3b1875f Mon Sep 17 00:00:00 2001 From: andersonid Date: Sun, 13 Apr 2025 22:30:05 -0300 Subject: [PATCH 1/9] =?UTF-8?q?fix:=20Corre=C3=A7=C3=B5es=20na=20p=C3=A1gi?= =?UTF-8?q?na=20de=20administra=C3=A7=C3=A3o=20e=20suas=20depend=C3=AAncia?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 214 ++++++++++++++++++++++++++++----- create_admin.py | 9 ++ functions/database.py | 9 +- functions/decorators.py | 54 +++++++-- templates/base.html | 7 +- templates/dashboard_admin.html | 211 +++++++++++++++++++++++++------- 6 files changed, 418 insertions(+), 86 deletions(-) diff --git a/app.py b/app.py index 531bb7a..8e36537 100644 --- a/app.py +++ b/app.py @@ -128,7 +128,30 @@ def create_app(): if 'user_id' not in session: flash('Por favor, faça login para acessar esta página.', 'warning') return redirect(url_for('login')) - return f(*args, **kwargs) + + from sqlalchemy.orm import Session + db = get_db_connection() + try: + # Carregar o usuário com suas roles e permissões + user = db.query(Usuario).options( + joinedload(Usuario.roles).joinedload(Role.permissions), + joinedload(Usuario.militante), + joinedload(Usuario.cr), + joinedload(Usuario.setor), + joinedload(Usuario.celula) + ).get(session['user_id']) + + if not user: + flash('Usuário não encontrado.', 'danger') + 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 # Decorator para verificar se a sessão expirou @@ -222,6 +245,7 @@ def create_app(): session['user_id'] = user.id session['username'] = user.username 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 return redirect(url_for("home")) @@ -1361,34 +1385,36 @@ def create_app(): @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!'}) + if not current_user.is_admin: + return jsonify({ + 'success': False, + 'error': 'Você não tem permissão para alterar o status de usuários.' + }), 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.ativo = not usuario.ativo + db.commit() + + return jsonify({ + 'success': True, + 'message': f'Usuário {"ativado" if usuario.ativo else "desativado"} com sucesso!' + }) + except Exception as e: + db.rollback() + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + finally: + db.close() @app.route('/usuarios//alterar_nivel', methods=['POST']) @require_login @@ -1619,6 +1645,136 @@ 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/create_admin.py b/create_admin.py index 9d70e6d..b53fa93 100644 --- a/create_admin.py +++ b/create_admin.py @@ -70,6 +70,15 @@ def create_admin_user(): admin.set_password("admin123") admin.generate_otp_secret() + # Buscar ou criar role de admin + admin_role = db.query(Role).filter_by(nome="admin").first() + if not admin_role: + admin_role = Role(nome="admin", nivel=0) # Nível 0 é o mais alto + db.add(admin_role) + + # Adicionar role ao usuário + admin.roles.append(admin_role) + # Adicionar e fazer commit db.add(admin) db.commit() diff --git a/functions/database.py b/functions/database.py index 54e0d1c..4f09e71 100644 --- a/functions/database.py +++ b/functions/database.py @@ -441,6 +441,7 @@ class Usuario(Base, UserMixin): username = Column(String(50), unique=True, nullable=False) password_hash = Column(String(255), nullable=False) email = Column(String(100), unique=True, nullable=False) + nome = Column(String(100)) # Nome completo do usuário otp_secret = Column(String(32)) role_id = Column(Integer, ForeignKey('roles.id')) setor_id = Column(Integer, ForeignKey('setores.id')) @@ -464,11 +465,11 @@ class Usuario(Base, UserMixin): cr = relationship('ComiteRegional', back_populates='usuarios') celula = relationship('Celula', back_populates='usuarios') - def __init__(self, username, email=None, is_admin=False): + def __init__(self, username, email=None, is_admin=False, nome=None): self.username = username self.email = email self.is_admin = is_admin - self.email = email + self.nome = nome self.ativo = True self.session_timeout = 30 self.tipo = "USUARIO" @@ -549,6 +550,10 @@ class Usuario(Base, UserMixin): self.motivo_logout = "Logout manual" self.ultima_atividade = None + def is_admin_user(self): + """Verifica se o usuário é admin""" + return self.is_admin or any(role.nome == "admin" for role in self.roles) + class PagamentoCelula(Base): __tablename__ = 'pagamentos_celula' diff --git a/functions/decorators.py b/functions/decorators.py index b2ec0dc..1483835 100644 --- a/functions/decorators.py +++ b/functions/decorators.py @@ -2,7 +2,7 @@ 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 .database import get_db_connection, Usuario, Role from .rbac import Permission def require_login(f): @@ -15,9 +15,13 @@ def require_login(f): db = get_db_connection() try: - # Carregar o usuário com suas roles + # Carregar o usuário com suas roles e permissões user = db.query(Usuario).options( - joinedload(Usuario.roles) + joinedload(Usuario.roles).joinedload(Role.permissions), + joinedload(Usuario.militante), + joinedload(Usuario.cr), + joinedload(Usuario.setor), + joinedload(Usuario.celula) ).get(current_user.id) if not user: @@ -28,7 +32,15 @@ def require_login(f): user.update_last_activity() db.commit() + # Substituir o current_user pelo usuário carregado + setattr(current_user, '_get_current_object', lambda: user) + + # Executar a função com o usuário carregado return f(*args, **kwargs) + except Exception as e: + db.rollback() + flash('Erro ao carregar dados do usuário.', 'danger') + return redirect(url_for('login')) finally: db.close() return decorated_function @@ -39,14 +51,38 @@ def require_permission(permission_name): @wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: - flash('Por favor, faça login para acessar esta página.', 'danger') + flash('Você precisa estar logado para acessar esta página.', 'error') return redirect(url_for('login')) - if not current_user.has_permission(permission_name): - flash('Você não tem permissão para acessar esta página.', 'danger') - return redirect(url_for('home')) - - return f(*args, **kwargs) + db = get_db_connection() + try: + # Carregar o usuário com suas roles e permissões + user = db.query(Usuario).options( + joinedload(Usuario.roles).joinedload(Role.permissions), + joinedload(Usuario.militante), + joinedload(Usuario.cr), + joinedload(Usuario.setor), + joinedload(Usuario.celula) + ).get(current_user.id) + + if not user: + flash('Usuário não encontrado.', 'error') + return redirect(url_for('login')) + + if 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() + + # Substituir o current_user pelo usuário carregado + setattr(current_user, '_get_current_object', lambda: user) + + return f(*args, **kwargs) + finally: + db.close() return decorated_function return decorator diff --git a/templates/base.html b/templates/base.html index 9327f54..c6f072b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -10,7 +10,7 @@ - + @@ -599,6 +599,11 @@ Novo Usuário +
  • + + Administração + +
  • {% endif %}
  • diff --git a/templates/dashboard_admin.html b/templates/dashboard_admin.html index d9c539a..170360d 100644 --- a/templates/dashboard_admin.html +++ b/templates/dashboard_admin.html @@ -1,63 +1,75 @@ -{% extends 'base.html' %} +{% 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
    +
    +
    +
    +

    Administração de Usuários

    + +
    - - +
    + - - - + + + + {% for usuario in usuarios %} - + + + - {% endfor %} @@ -66,18 +78,127 @@ - -
    -
    -
    Ações Rápidas
    -
    -
    - + + + + +{% endblock %} + +{% block scripts %} + {% endblock %} \ No newline at end of file From 5057802220350a2c4204d559452f1422fd13b9b8 Mon Sep 17 00:00:00 2001 From: andersonid Date: Sun, 13 Apr 2025 22:48:27 -0300 Subject: [PATCH 2/9] =?UTF-8?q?tela=20de=20administra=C3=A7=C3=A3o=20em=20?= =?UTF-8?q?ajustes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 92bc21dbd8b7fd9cf6dd3da127700a2e0b896f27 Mon Sep 17 00:00:00 2001 From: andersonid Date: Tue, 15 Apr 2025 10:19:59 -0300 Subject: [PATCH 3/9] =?UTF-8?q?Tela=20inicial=20de=20administra=C3=A7?= =?UTF-8?q?=C3=A3o=20desenvolvida.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/components.css | 15 +++++++++++++++ templates/dashboard_admin.html | 10 ++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/static/css/components.css b/static/css/components.css index 31c632a..bcec453 100644 --- a/static/css/components.css +++ b/static/css/components.css @@ -20,6 +20,10 @@ --bs-success-dark: #157347; --bs-secondary: #6c757d; --bs-secondary-dark: #565e64; + + /* Variáveis para status */ + --status-active: #28a745; + --status-inactive: #dc3545; } /* Tabelas */ @@ -608,4 +612,15 @@ input.btn-secondary:hover, color: #055160; background-color: #cff4fc; border-color: #b6effb; +} + +/* Status styles */ +.status-active { + color: var(--status-active); + font-weight: 500; +} + +.status-inactive { + color: var(--status-inactive); + font-weight: 500; } \ No newline at end of file diff --git a/templates/dashboard_admin.html b/templates/dashboard_admin.html index 170360d..01a549a 100644 --- a/templates/dashboard_admin.html +++ b/templates/dashboard_admin.html @@ -4,15 +4,9 @@ {% block content %}
    +

    Administração de Usuários

    -
    -

    Administração de Usuários

    -
    - -
    ID Usuário EmailAdminOTP ConfiguradoNomeÚltimo AcessoStatusNível Ações
    {{ usuario.id }} {{ usuario.username }} {{ usuario.email }}{{ usuario.nome }}{{ usuario.last_login }} + + {{ "Ativo" if usuario.ativo else "Inativo" }} + + {% if usuario.is_admin %} - Sim + Administrador {% else %} - Não + {{ usuario.nivel }} {% endif %} - {% if usuario.otp_secret %} - Sim - {% else %} - Não - {% endif %} - -
    - -
    + + + + {% if not usuario.is_admin %} + + {% endif %} +
    @@ -34,7 +28,7 @@ From 53769cf080014a6aff11d40ea90e0e6705f06a44 Mon Sep 17 00:00:00 2001 From: andersonid Date: Tue, 15 Apr 2025 10:21:41 -0300 Subject: [PATCH 4/9] =?UTF-8?q?Adicionado=20par=C3=A2metro=20de=20vers?= =?UTF-8?q?=C3=A3o=20aleat=C3=B3rio=20no=20CSS=20para=20evitar=20cache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/base.html b/templates/base.html index c6f072b..c938f5b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -12,7 +12,7 @@ - + +{% endblock %} \ No newline at end of file diff --git a/templates/admin/dashboard.html b/templates/admin/dashboard.html new file mode 100644 index 0000000..fa305e4 --- /dev/null +++ b/templates/admin/dashboard.html @@ -0,0 +1,79 @@ +{% extends "admin/base.html" %} + +{% block admin_title %}Dashboard Administrativo{% endblock %} + +{% block admin_content %} +
    + +
    +
    +
    +
    +
    +
    + Total de Usuários
    +
    {{ total_users }}
    +
    +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + Usuários Ativos
    +
    {{ active_users }}
    +
    +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + Usuários Inativos
    +
    {{ inactive_users }}
    +
    +
    + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    Ações Rápidas
    +
    + +
    +
    +
    + +{% endblock %} \ No newline at end of file diff --git a/templates/admin/users.html b/templates/admin/users.html new file mode 100644 index 0000000..c3c6096 --- /dev/null +++ b/templates/admin/users.html @@ -0,0 +1,108 @@ +{% extends "admin/base.html" %} + +{% block admin_title %}Gerenciamento de Usuários{% endblock %} + +{% block admin_content %} +
    +
    +
    Lista de Usuários
    +
    +
    +
    +
    {{ usuario.nome }} {{ usuario.last_login }} - + {{ "Ativo" if usuario.ativo else "Inativo" }}
    + + + + + + + + + + + {% 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 From 0f32eae5cf9b13961e3b3a6b75c17ef31e21c7aa Mon Sep 17 00:00:00 2001 From: andersonid Date: Tue, 15 Apr 2025 14:26:02 -0300 Subject: [PATCH 6/9] =?UTF-8?q?refactor(#11):=20Integra=20listagem=20de=20?= =?UTF-8?q?usu=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 From 8255f1d93312032609639f044011b9e2e68323d2 Mon Sep 17 00:00:00 2001 From: andersonid Date: Tue, 15 Apr 2025 15:09:03 -0300 Subject: [PATCH 7/9] =?UTF-8?q?feat(#11):=20Adiciona=20m=C3=B3dulo=20creat?= =?UTF-8?q?e=5Ftest=5Fusers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 4 +- create_test_users.py | 56 ++++++++ routes/admin.py | 16 ++- templates/admin/dashboard.html | 250 ++++++++++++++++++++++++--------- 4 files changed, 246 insertions(+), 80 deletions(-) create mode 100644 create_test_users.py diff --git a/app.py b/app.py index 85ac6ab..64bc675 100644 --- a/app.py +++ b/app.py @@ -251,9 +251,7 @@ 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 admin.dashboard se for admin, senão para home - if user.is_admin: - return redirect(url_for("admin.dashboard")) + # Redirecionar para home return redirect(url_for("home")) finally: db.close() diff --git a/create_test_users.py b/create_test_users.py new file mode 100644 index 0000000..5774f8c --- /dev/null +++ b/create_test_users.py @@ -0,0 +1,56 @@ +from functions.database import get_db_connection, Usuario, Role +from werkzeug.security import generate_password_hash + +def create_test_users(): + """Cria usuários de teste""" + db = get_db_connection() + try: + # Lista de usuários de teste + test_users = [ + { + 'username': 'aligner', + 'email': 'aligner@test.com', + 'password': 'Test123!@#', + 'is_admin': False + }, + { + 'username': 'tester', + 'email': 'tester@test.com', + 'password': 'Test123!@#', + 'is_admin': False + }, + { + 'username': 'deployer', + 'email': 'deployer@test.com', + 'password': 'Test123!@#', + 'is_admin': False + } + ] + + # Criar cada usuário + for user_data in test_users: + user = db.query(Usuario).filter_by(username=user_data['username']).first() + + if not user: + user = Usuario( + username=user_data['username'], + email=user_data['email'], + is_admin=user_data['is_admin'] + ) + user.set_password(user_data['password']) + db.add(user) + print(f"Usuário {user_data['username']} criado") + else: + print(f"Usuário {user_data['username']} já existe") + + db.commit() + print("Usuários de teste criados com sucesso") + + except Exception as e: + print(f"Erro ao criar usuários de teste: {str(e)}") + db.rollback() + finally: + db.close() + +if __name__ == "__main__": + create_test_users() \ No newline at end of file diff --git a/routes/admin.py b/routes/admin.py index f9631db..232e307 100644 --- a/routes/admin.py +++ b/routes/admin.py @@ -9,6 +9,7 @@ import secrets from functools import wraps from sqlalchemy.exc import SQLAlchemyError import logging +from datetime import datetime logger = logging.getLogger(__name__) @@ -30,6 +31,8 @@ def dashboard(): """Dashboard principal da área administrativa com lista de usuários""" db = get_db_connection() try: + now = datetime.now() + # Carregar estatísticas relevantes total_users = db.query(Usuario).count() active_users = db.query(Usuario).filter(Usuario.is_active == True).count() @@ -46,7 +49,8 @@ def dashboard(): total_users=total_users, active_users=active_users, inactive_users=inactive_users, - users=users + users=users, + now=now ) except SQLAlchemyError as e: logger.error(f"Erro ao buscar dados do dashboard: {str(e)}") @@ -111,16 +115,14 @@ def toggle_user_status(user_id): try: user = db.query(Usuario).get(user_id) if not user: - return jsonify({'success': False, 'message': 'Usuário não encontrado.'}) + flash('Usuário não encontrado.', 'danger') + return redirect(url_for('admin.dashboard')) user.is_active = not user.is_active db.commit() status = 'ativado' if user.is_active else 'desativado' - return jsonify({ - 'success': True, - 'message': f'Usuário {status} com sucesso.', - 'new_status': user.is_active - }) + flash(f'Usuário {status} com sucesso.', 'success') + return redirect(url_for('admin.dashboard')) finally: db.close() \ No newline at end of file diff --git a/templates/admin/dashboard.html b/templates/admin/dashboard.html index 9931243..1ce3ae5 100644 --- a/templates/admin/dashboard.html +++ b/templates/admin/dashboard.html @@ -2,115 +2,225 @@ {% block title %}Dashboard Administrativo{% endblock %} +{% block extra_css %} + +{% endblock %} + {% block content %} +

    + + Administração de Usuários +

    +
    -
    +
    -
    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 }}

    + +
    -
    -
    -
    Gerenciamento de Usuários
    +
    +
    +
    + + Lista 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' }} +
    + + + + + + + + + + + + {% for user in users %} + + + + + + - - {% endfor %} - -
    NomeEmailStatusÚltimo LoginAções
    {{ user.name }}{{ user.email }} + + {{ "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 %} From e6057cd5661efb0297b907d634204a8b8f9e7972 Mon Sep 17 00:00:00 2001 From: andersonid Date: Tue, 15 Apr 2025 15:10:21 -0300 Subject: [PATCH 8/9] =?UTF-8?q?fix(#11):=20Corrige=20inicializa=C3=A7?= =?UTF-8?q?=C3=A3o=20do=20sistema=20para=20n=C3=A3o=20recriar=20usu=C3=A1r?= =?UTF-8?q?ios=20a=20cada=20execu=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 5 ++++- app.py | 19 ++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index a9a5333..9292d78 100644 --- a/Makefile +++ b/Makefile @@ -11,10 +11,13 @@ init-db: clean seed: init-db python seed.py +init: + python app.py --init + run: python app.py -run-with-seed: seed run +run-with-seed: seed init run reset-admin: clean python create_admin.py diff --git a/app.py b/app.py index 64bc675..e50bb1f 100644 --- a/app.py +++ b/app.py @@ -52,6 +52,7 @@ from flask_wtf.csrf import CSRFProtect import json from utils.date_utils import validar_data, converter_data, validar_sequencia_datas, calcular_idade from routes.admin import admin_bp # Importar o blueprint administrativo +import sys load_dotenv() @@ -1713,18 +1714,18 @@ def init_system(): def main(): # Criar a aplicação app = create_app() - - # Inicializar o sistema - init_system() - return app # Criar a aplicação usando a função main app = main() if __name__ == '__main__': - app.run( - host='0.0.0.0', - port=5000, - debug=os.getenv('FLASK_ENV') == 'development' - ) + # Verificar se é para inicializar o sistema + if '--init' in sys.argv: + init_system() + else: + app.run( + host='0.0.0.0', + port=5000, + debug=os.getenv('FLASK_ENV') == 'development' + ) From 2da8dec63fcbcb4069bbc94fee6a6fde9e4b9db3 Mon Sep 17 00:00:00 2001 From: andersonid Date: Fri, 25 Apr 2025 18:29:59 -0300 Subject: [PATCH 9/9] =?UTF-8?q?Primeira=20parte=20da=20tela=20de=20adminis?= =?UTF-8?q?tra=C3=A7=C3=A3o=20de=20usu=C3=A1rios=20do=20sistema.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit