From 47f13e7c188b5f72372227991ba491c2dba5c051 Mon Sep 17 00:00:00 2001 From: andersonid Date: Tue, 15 Apr 2025 10:49:15 -0300 Subject: [PATCH] =?UTF-8?q?feat(#11):=20Implementa=20estrutura=20inicial?= =?UTF-8?q?=20da=20=C3=A1rea=20administrativa=20-=20Cria=20blueprint=20adm?= =?UTF-8?q?inistrativo=20com=20rotas=20b=C3=A1sicas=20-=20Implementa=20tem?= =?UTF-8?q?plates=20base=20para=20=C3=A1rea=20administrativa=20-=20Adicion?= =?UTF-8?q?a=20dashboard=20administrativo=20-=20Implementa=20gerenciamento?= =?UTF-8?q?=20de=20usu=C3=A1rios=20-=20Organiza=20rotas=20em=20pacote=20se?= =?UTF-8?q?parado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 4 ++ routes/__init__.py | 2 + routes/admin.py | 112 +++++++++++++++++++++++++++++++++ templates/admin/base.html | 100 +++++++++++++++++++++++++++++ templates/admin/dashboard.html | 79 +++++++++++++++++++++++ templates/admin/users.html | 108 +++++++++++++++++++++++++++++++ 6 files changed, 405 insertions(+) create mode 100644 routes/__init__.py create mode 100644 routes/admin.py create mode 100644 templates/admin/base.html create mode 100644 templates/admin/dashboard.html create mode 100644 templates/admin/users.html diff --git a/app.py b/app.py index 8e36537..47a4d63 100644 --- a/app.py +++ b/app.py @@ -51,6 +51,7 @@ from sqlalchemy.sql import func 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 load_dotenv() @@ -59,6 +60,9 @@ def create_app(): app.secret_key = os.getenv('SECRET_KEY', secrets.token_hex(16)) bootstrap = Bootstrap5(app) + # Registrar o blueprint administrativo + app.register_blueprint(admin_bp) + # Configurar CSRF Protection csrf = CSRFProtect() csrf.init_app(app) diff --git a/routes/__init__.py b/routes/__init__.py new file mode 100644 index 0000000..5d588a4 --- /dev/null +++ b/routes/__init__.py @@ -0,0 +1,2 @@ +# Este arquivo está intencionalmente vazio +# Ele é usado para marcar o diretório como um pacote Python \ No newline at end of file diff --git a/routes/admin.py b/routes/admin.py new file mode 100644 index 0000000..f8fda8a --- /dev/null +++ b/routes/admin.py @@ -0,0 +1,112 @@ +from flask import Blueprint, render_template, flash, redirect, url_for, request, jsonify +from functions.database import Usuario, get_db_connection +from functions.decorators import require_permission, require_role, require_minimum_role +from flask_login import login_required, current_user +from sqlalchemy.orm import joinedload +import pyotp +from werkzeug.security import generate_password_hash +import secrets + +admin_bp = Blueprint('admin', __name__, url_prefix='/admin') + +@admin_bp.route('/') +@login_required +@require_role('ADMIN') +def dashboard(): + """Dashboard principal da área administrativa""" + db = get_db_connection() + try: + # Carregar estatísticas relevantes + total_users = db.query(Usuario).count() + 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: + users = db.query(Usuario).options( + joinedload(Usuario.roles), + joinedload(Usuario.militante) + ).all() + return render_template('admin/users.html', users=users) + finally: + db.close() + +@admin_bp.route('/users//reset-otp', methods=['POST']) +@login_required +@require_role('ADMIN') +def reset_user_otp(user_id): + """Reseta o OTP de um usuário""" + db = get_db_connection() + try: + user = db.query(Usuario).get(user_id) + if not user: + flash('Usuário não encontrado.', 'danger') + return redirect(url_for('admin.list_users')) + + # 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')) + finally: + db.close() + +@admin_bp.route('/users//reset-password', methods=['POST']) +@login_required +@require_role('ADMIN') +def reset_user_password(user_id): + """Reseta a senha de um usuário""" + db = get_db_connection() + try: + user = db.query(Usuario).get(user_id) + if not user: + flash('Usuário não encontrado.', 'danger') + return redirect(url_for('admin.list_users')) + + # Gerar nova senha aleatória + new_password = secrets.token_urlsafe(8) + user.password = generate_password_hash(new_password) + db.commit() + + flash(f'Senha resetada com sucesso. Nova senha: {new_password}', 'success') + return redirect(url_for('admin.list_users')) + finally: + db.close() + +@admin_bp.route('/users//toggle-status', methods=['POST']) +@login_required +@require_role('ADMIN') +def toggle_user_status(user_id): + """Ativa/desativa 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.'}) + + 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 + }) + finally: + db.close() \ No newline at end of file diff --git a/templates/admin/base.html b/templates/admin/base.html new file mode 100644 index 0000000..045cf14 --- /dev/null +++ b/templates/admin/base.html @@ -0,0 +1,100 @@ +{% extends "base.html" %} + +{% block title %}Área Administrativa{% endblock %} + +{% block content %} +
+
+ + + + +
+
+

{% block admin_title %}{% endblock %}

+
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + + {% block admin_content %}{% endblock %} +
+
+
+{% endblock %} + +{% block extra_css %} + +{% 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
+
+
+
+ + + + + + + + + + + + {% 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