Refactoring and cleanup codebase (50%...)

MVP dashboard users page
This commit is contained in:
Udo Chudo 2025-03-17 15:30:58 +05:00
parent 20465600b1
commit 9e2560f7c3
32 changed files with 937 additions and 423 deletions

View File

@ -9,3 +9,4 @@
/db/ /db/
/db/telezab.db /db/telezab.db
/trash/ /trash/
/venv3.12.3/

1
.gitignore vendored
View File

@ -11,3 +11,4 @@
/logs/error.log /logs/error.log
/db/ /db/
/db/telezab.db /db/telezab.db
/venv3.12.3/

View File

@ -1,23 +1,21 @@
import sqlite3 import sqlite3
import telebot import telebot
import telezab import telezab
from backend_locks import db_lock, bot from backend_locks import db_lock, bot
from bot_database import get_admins, is_whitelisted, format_regions_list, get_sorted_regions, log_user_event, \ from bot_database import get_admins, is_whitelisted, format_regions_list, get_sorted_regions, log_user_event, \
get_user_subscribed_regions get_user_subscribed_regions
from config import DB_PATH from config import DB_PATH
from telezab import handle_my_subscriptions_button, handle_active_regions_button, handle_notification_mode_button from telezab import handle_my_subscriptions_button, handle_active_regions_button, handle_notification_mode_button
from utils import show_main_menu, show_settings_menu from utilities.telegram_utilities import show_main_menu, show_settings_menu
def handle_main_menu(message, chat_id, text): def handle_main_menu(message, chat_id, text):
"""Обработка команд в главном меню.""" """Обработка команд в главном меню."""
if text == 'Регистрация': if text == 'Регистрация':
telezab.user_state_manager.set_state(chat_id, "REGISTRATION") telezab.state.set_state(chat_id, "REGISTRATION")
telezab.handle_register(message) telezab.handle_register(message)
elif text == 'Настройки': elif text == 'Настройки':
telezab.user_state_manager.set_state(chat_id, "SETTINGS_MENU") telezab.state.set_state(chat_id, "SETTINGS_MENU")
telezab.show_settings_menu(chat_id) telezab.show_settings_menu(chat_id)
elif text == 'Помощь': elif text == 'Помощь':
telezab.handle_help(message) telezab.handle_help(message)
@ -32,10 +30,10 @@ def handle_settings_menu(message, chat_id, text):
"""Обработка команд в меню настроек.""" """Обработка команд в меню настроек."""
admins_list = get_admins() admins_list = get_admins()
if text.lower() == 'подписаться': if text.lower() == 'подписаться':
telezab.user_state_manager.set_state(chat_id, "SUBSCRIBE") telezab.state.set_state(chat_id, "SUBSCRIBE")
handle_subscribe_button(message) handle_subscribe_button(message)
elif text.lower() == 'отписаться': elif text.lower() == 'отписаться':
telezab.user_state_manager.set_state(chat_id, "UNSUBSCRIBE") telezab.state.set_state(chat_id, "UNSUBSCRIBE")
handle_unsubscribe_button(message) handle_unsubscribe_button(message)
elif text.lower() == 'мои подписки': elif text.lower() == 'мои подписки':
handle_my_subscriptions_button(message) handle_my_subscriptions_button(message)
@ -44,7 +42,7 @@ def handle_settings_menu(message, chat_id, text):
elif text.lower() == "режим уведомлений": elif text.lower() == "режим уведомлений":
handle_notification_mode_button(message) handle_notification_mode_button(message)
elif text.lower() == 'назад': elif text.lower() == 'назад':
telezab.user_state_manager.set_state(chat_id, "MAIN_MENU") telezab.state.set_state(chat_id, "MAIN_MENU")
show_main_menu(chat_id) show_main_menu(chat_id)
else: else:
bot.send_message(chat_id, "Команда не распознана.") bot.send_message(chat_id, "Команда не распознана.")
@ -75,7 +73,7 @@ def process_subscription_button(message, chat_id, username):
invalid_regions = [] invalid_regions = []
if message.text.lower() == 'отмена': if message.text.lower() == 'отмена':
bot.send_message(chat_id, "Действие отменено.") bot.send_message(chat_id, "Действие отменено.")
telezab.user_state_manager.set_state(chat_id, "SETTINGS_MENU") telezab.state.set_state(chat_id, "SETTINGS_MENU")
return show_settings_menu(chat_id) return show_settings_menu(chat_id)
if not all(part.strip().isdigit() for part in message.text.split(',')): if not all(part.strip().isdigit() for part in message.text.split(',')):
markup = telebot.types.InlineKeyboardMarkup() markup = telebot.types.InlineKeyboardMarkup()
@ -108,7 +106,7 @@ def process_subscription_button(message, chat_id, username):
f"Регион с ID {', '.join(invalid_regions)} не существует. Введите корректные номера или 'отмена'.") f"Регион с ID {', '.join(invalid_regions)} не существует. Введите корректные номера или 'отмена'.")
bot.send_message(chat_id, f"Подписка на регионы: {', '.join(subbed_regions)} оформлена.") bot.send_message(chat_id, f"Подписка на регионы: {', '.join(subbed_regions)} оформлена.")
log_user_event(chat_id, username, f"Subscribed to regions: {', '.join(subbed_regions)}") log_user_event(chat_id, username, f"Subscribed to regions: {', '.join(subbed_regions)}")
telezab.user_state_manager.set_state(chat_id, "SETTINGS_MENU") telezab.state.set_state(chat_id, "SETTINGS_MENU")
show_settings_menu(chat_id) show_settings_menu(chat_id)
@ -117,7 +115,7 @@ def handle_unsubscribe_button(message):
if not is_whitelisted(chat_id): if not is_whitelisted(chat_id):
bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.") bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.")
telebot.logger.info(f"Unauthorized access attempt by {chat_id}") telebot.logger.info(f"Unauthorized access attempt by {chat_id}")
telezab.user_state_manager.set_state(chat_id, "REGISTRATION") telezab.state.set_state(chat_id, "REGISTRATION")
return show_main_menu(chat_id) return show_main_menu(chat_id)
username = message.from_user.username username = message.from_user.username
if username: if username:
@ -129,7 +127,7 @@ def handle_unsubscribe_button(message):
if not user_regions: if not user_regions:
bot.send_message(chat_id, "Вы не подписаны ни на один регион.") bot.send_message(chat_id, "Вы не подписаны ни на один регион.")
telezab.user_state_manager.set_state(chat_id, "SETTINGS_MENU") telezab.state.set_state(chat_id, "SETTINGS_MENU")
return show_settings_menu(chat_id) return show_settings_menu(chat_id)
regions_list = format_regions_list(user_regions) regions_list = format_regions_list(user_regions)
markup = telebot.types.InlineKeyboardMarkup() markup = telebot.types.InlineKeyboardMarkup()
@ -147,7 +145,7 @@ def process_unsubscription_button(message, chat_id, username):
markup.add(telebot.types.InlineKeyboardButton(text="Отмена", callback_data=f"cancel_action")) markup.add(telebot.types.InlineKeyboardButton(text="Отмена", callback_data=f"cancel_action"))
if message.text.lower() == 'отмена': if message.text.lower() == 'отмена':
bot.send_message(chat_id, "Действие отменено.") bot.send_message(chat_id, "Действие отменено.")
telezab.user_state_manager.set_state(chat_id, "SETTINGS_MENU") telezab.state.set_state(chat_id, "SETTINGS_MENU")
return show_settings_menu(chat_id) return show_settings_menu(chat_id)
# Проверка, что введённая строка содержит только цифры и запятые # Проверка, что введённая строка содержит только цифры и запятые
if not all(part.strip().isdigit() for part in message.text.split(',')): if not all(part.strip().isdigit() for part in message.text.split(',')):
@ -173,5 +171,5 @@ def process_unsubscription_button(message, chat_id, username):
bot.send_message(chat_id, f"Регион с ID {', '.join(invalid_regions)} не найден в ваших подписках.") bot.send_message(chat_id, f"Регион с ID {', '.join(invalid_regions)} не найден в ваших подписках.")
bot.send_message(chat_id, f"Отписка от регионов: {', '.join(unsubbed_regions)} выполнена.") bot.send_message(chat_id, f"Отписка от регионов: {', '.join(unsubbed_regions)} выполнена.")
log_user_event(chat_id, username, f"Unsubscribed from regions: {', '.join(unsubbed_regions)}") log_user_event(chat_id, username, f"Unsubscribed from regions: {', '.join(unsubbed_regions)}")
telezab.user_state_manager.set_state(chat_id, "SETTINGS_MENU") telezab.state.set_state(chat_id, "SETTINGS_MENU")
show_settings_menu(chat_id) show_settings_menu(chat_id)

View File

@ -2,18 +2,37 @@ import logging
import sqlite3 import sqlite3
import telebot import telebot
from flask import Flask, request, jsonify, render_template from flask import Flask, request, jsonify, render_template, flash, redirect, url_for
from flask_ldap3_login.forms import LDAPLoginForm
from flask_login import login_manager, login_user, logout_user, UserMixin
from frontend.dashboard import bp_dashboard, bp_api
import backend_bot import backend_bot
import bot_database import bot_database
import telezab import telezab
import utils import utilities.telegram_utilities as telegram_util
from backend_locks import db_lock from backend_locks import db_lock
from config import BASE_URL, DB_PATH from config import BASE_URL, DB_PATH
from utils import extract_region_number, format_message, validate_chat_id, validate_telegram_id, validate_email from utilities.telegram_utilities import extract_region_number, format_message, validate_chat_id, validate_telegram_id, validate_email
app = Flask(__name__, static_url_path='/telezab/static', template_folder='templates') app = Flask(__name__, static_url_path='/telezab/static', template_folder='templates')
# app.register_blueprint(webui) # app.register_blueprint(webui)
app.secret_key = "supersecretkey"
app.register_blueprint(bp_dashboard)
app.register_blueprint(bp_api)
#
# # Инициализация менеджеров
# ldap_manager = LDAP3LoginManager(app)
# login_manager = LoginManager(app)
# login_manager.login_view = "login"
# Пользовательский класс
class User(UserMixin):
def __init__(self, dn, username):
self.id = dn
self.username = username
# Настройка уровня логирования для Flask # Настройка уровня логирования для Flask
app.logger.setLevel(logging.INFO) app.logger.setLevel(logging.INFO)
@ -26,9 +45,9 @@ def webhook():
data = request.get_json() data = request.get_json()
app.logger.info(f"Получены данные: {data}") app.logger.info(f"Получены данные: {data}")
# Генерация хеша события и логирование # # Генерация хеша события и логирование
event_hash = bot_database.hash_data(data) # event_hash = bot_database.hash_data(data)
app.logger.debug(f"Сгенерирован хеш для события: {event_hash}") # app.logger.debug(f"Сгенерирован хеш для события: {event_hash}")
# Работа с базой данных в блоке синхронизации # Работа с базой данных в блоке синхронизации
with db_lock: with db_lock:
@ -82,7 +101,7 @@ def webhook():
app.logger.info( app.logger.info(
f"Формирование сообщения для пользователя {username} (chat_id={chat_id}) [{formatted_message}]") f"Формирование сообщения для пользователя {username} (chat_id={chat_id}) [{formatted_message}]")
try: try:
from rabbitmq import send_to_queue from utilities.rabbitmq import send_to_queue
send_to_queue({'chat_id': chat_id, 'username': username, 'message': message}) send_to_queue({'chat_id': chat_id, 'username': username, 'message': message})
app.logger.debug(f"Сообщение поставлено в очередь для {chat_id} (@{username})") app.logger.debug(f"Сообщение поставлено в очередь для {chat_id} (@{username})")
except Exception as e: except Exception as e:
@ -93,8 +112,8 @@ def webhook():
if undelivered: if undelivered:
query = 'INSERT OR IGNORE INTO events (hash, data, delivered) VALUES (?, ?, ?)' query = 'INSERT OR IGNORE INTO events (hash, data, delivered) VALUES (?, ?, ?)'
app.logger.debug( app.logger.debug(
f"Сохранение события в базе данных: {query} (hash={event_hash}, delivered={False})") f"Сохранение события в базе данных: {query} (delivered={False})")
cursor.execute(query, (event_hash, str(data), False)) cursor.execute(query, (str(data), False))
# Коммитим изменения в базе данных # Коммитим изменения в базе данных
conn.commit() conn.commit()
@ -152,11 +171,11 @@ def add_user():
if success: if success:
# INFO: Пользователь успешно добавлен в whitelist # INFO: Пользователь успешно добавлен в whitelist
app.logger.info(f"Пользователь {telegram_id} добавлен в whitelist.") app.logger.info(f"Пользователь {telegram_id} добавлен в whitelist.")
telezab.user_state_manager.set_state(chat_id, "MAIN_MENU") telezab.state.set_state(chat_id, "MAIN_MENU")
# DEBUG: Показ основного меню пользователю # DEBUG: Показ основного меню пользователю
app.logger.debug(f"Отображение основного меню для пользователя с chat_id {chat_id}") app.logger.debug(f"Отображение основного меню для пользователя с chat_id {chat_id}")
utils.show_main_menu(chat_id) telegram_util.show_main_menu(chat_id)
return jsonify( return jsonify(
{"status": "success", "msg": f"User {telegram_id} with {user_email} added successfully"}), 200 {"status": "success", "msg": f"User {telegram_id} with {user_email} added successfully"}), 200
else: else:
@ -238,76 +257,71 @@ def delete_user():
app.logger.debug(f"Соединение с базой данных закрыто") app.logger.debug(f"Соединение с базой данных закрыто")
@app.route(BASE_URL + '/users/get', methods=['GET']) # @app.route(BASE_URL + '/users/get', methods=['GET'])
def get_users(): # def get_users():
try: # try:
# INFO: Запрос на получение списка пользователей # # INFO: Запрос на получение списка пользователей
app.logger.info("Запрос на получение информации о пользователях получен") # app.logger.info("Запрос на получение информации о пользователях получен")
#
with db_lock: # with db_lock:
conn = sqlite3.connect(DB_PATH) # conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor() # cursor = conn.cursor()
#
# DEBUG: Запрос данных из таблицы whitelist # # DEBUG: Запрос данных из таблицы whitelist
app.logger.debug("Запрос данных пользователей из таблицы whitelist") # app.logger.debug("Запрос данных пользователей из таблицы whitelist")
cursor.execute('SELECT * FROM whitelist') # cursor.execute('SELECT * FROM whitelist')
users = cursor.fetchall() # users = cursor.fetchall()
app.logger.debug("Формирование словаря пользователей") # app.logger.debug("Формирование словаря пользователей")
users_dict = {user_id: {'id': user_id, 'username': username, 'email': email, 'events': [], 'worker': '', # users_dict = {user_id: {'id': user_id, 'username': username, 'email': email, 'events': [], 'worker': '',
'subscriptions': []} # 'subscriptions': []}
for user_id, username, email in users} # for user_id, username, email in users}
#
# DEBUG: Запрос данных событий пользователей # # DEBUG: Запрос данных событий пользователей
app.logger.debug("Запрос событий пользователей из таблицы user_events") # app.logger.debug("Запрос событий пользователей из таблицы user_events")
cursor.execute('SELECT chat_id, username, action, timestamp FROM user_events') # cursor.execute('SELECT chat_id, username, action, timestamp FROM user_events')
events = cursor.fetchall() # events = cursor.fetchall()
#
# DEBUG: Обработка событий и добавление их в словарь пользователей # # DEBUG: Обработка событий и добавление их в словарь пользователей
for chat_id, username, action, timestamp in events: # for chat_id, username, action, timestamp in events:
if chat_id in users_dict: # if chat_id in users_dict:
event = {'type': action, 'date': timestamp} # event = {'type': action, 'date': timestamp}
if "Subscribed to region" in action: # if "Subscribed to region" in action:
region = action.split(": ")[-1] # region = action.split(": ")[-1]
event['region'] = region # event['region'] = region
users_dict[chat_id]['events'].append(event) # users_dict[chat_id]['events'].append(event)
#
# DEBUG: Запрос данных подписок пользователей # # DEBUG: Запрос данных подписок пользователей
app.logger.debug("Запрос активных подписок пользователей из таблицы subscriptions") # app.logger.debug("Запрос активных подписок пользователей из таблицы subscriptions")
cursor.execute('SELECT chat_id, region_id FROM subscriptions WHERE active = 1') # cursor.execute('SELECT chat_id, region_id FROM subscriptions WHERE active = 1')
subscriptions = cursor.fetchall() # subscriptions = cursor.fetchall()
#
# DEBUG: Добавление подписок к пользователям # # DEBUG: Добавление подписок к пользователям
for chat_id, region_id in subscriptions: # for chat_id, region_id in subscriptions:
if chat_id in users_dict: # if chat_id in users_dict:
users_dict[chat_id]['subscriptions'].append(str(region_id)) # users_dict[chat_id]['subscriptions'].append(str(region_id))
#
# INFO: Формирование результата # # INFO: Формирование результата
app.logger.info("Формирование результата для ответа") # app.logger.info("Формирование результата для ответа")
result = [] # result = []
for user in users_dict.values(): # for user in users_dict.values():
ordered_user = { # ordered_user = {
'email': user['email'], # 'email': user['email'],
'username': user['username'], # 'username': user['username'],
'id': user['id'], # 'id': user['id'],
'worker': user['worker'], # 'worker': user['worker'],
'events': user['events'], # 'events': user['events'],
'subscriptions': ', '.join(user['subscriptions']) # 'subscriptions': ', '.join(user['subscriptions'])
} # }
result.append(ordered_user) # result.append(ordered_user)
#
# INFO: Успешная отправка данных пользователей # # INFO: Успешная отправка данных пользователей
app.logger.info("Информация о пользователях успешно отправлена") # app.logger.info("Информация о пользователях успешно отправлена")
return jsonify(result) # return jsonify(result)
#
except Exception as e: # except Exception as e:
# ERROR: Ошибка при получении информации о пользователях # # ERROR: Ошибка при получении информации о пользователях
app.logger.error(f"Ошибка при получении информации о пользователях: {str(e)}") # app.logger.error(f"Ошибка при получении информации о пользователях: {str(e)}")
return jsonify({'status': 'error', 'message': str(e)}), 500 # return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route(BASE_URL + '/users', methods=['GET'])
def view_users():
return render_template('users.html')
@app.route(BASE_URL + '/debug/flask', methods=['POST']) @app.route(BASE_URL + '/debug/flask', methods=['POST'])

View File

@ -8,7 +8,7 @@ from pyzabbix import ZabbixAPI
import backend_bot import backend_bot
from config import ZABBIX_URL, ZABBIX_API_TOKEN from config import ZABBIX_URL, ZABBIX_API_TOKEN
from utils import show_main_menu from utilities.telegram_utilities import show_main_menu
def get_triggers_for_group(chat_id, group_id): def get_triggers_for_group(chat_id, group_id):

View File

@ -9,7 +9,14 @@ DB_PATH = 'db/telezab.db'
SUPPORT_EMAIL = "shiftsupport-rtmis@rtmis.ru" SUPPORT_EMAIL = "shiftsupport-rtmis@rtmis.ru"
BASE_URL = '/telezab' BASE_URL = '/telezab'
RABBITMQ_HOST = os.getenv('RABBITMQ_HOST') RABBITMQ_HOST = os.getenv('RABBITMQ_HOST')
RABBITMQ_QUEUE = 'telegram_notifications'
RABBITMQ_LOGIN = os.getenv('RABBITMQ_LOGIN') RABBITMQ_LOGIN = os.getenv('RABBITMQ_LOGIN')
RABBITMQ_PASS = os.getenv('RABBITMQ_PASS') RABBITMQ_PASS = os.getenv('RABBITMQ_PASS')
RABBITMQ_QUEUE = 'telegram_notifications'
RABBITMQ_URL_FULL = f"amqp://{RABBITMQ_LOGIN}:{RABBITMQ_PASS}@{RABBITMQ_HOST}/" RABBITMQ_URL_FULL = f"amqp://{RABBITMQ_LOGIN}:{RABBITMQ_PASS}@{RABBITMQ_HOST}/"
import os
class Config:
SQLALCHEMY_DATABASE_URI = f'sqlite:///{DB_PATH}'
SQLALCHEMY_TRACK_MODIFICATIONS = False
SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-key'

177
frontend/dashboard.py Normal file
View File

@ -0,0 +1,177 @@
from flask import Blueprint, render_template, jsonify, request, redirect, url_for
from flask_login import login_required
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine, text
from config import DB_PATH, BASE_URL
from .models import Region # Импортируем модель региона
# Создаём Blueprint
bp_dashboard = Blueprint('dashboard', __name__, url_prefix='/telezab/')
bp_api = Blueprint('api', __name__, url_prefix='/telezab/rest/api')
db_engine = create_engine(f'sqlite:///{DB_PATH}')
Session = sessionmaker(bind=db_engine)
# Роуты для отображения страниц
@bp_dashboard.route('/')
# @login_required
def dashboard():
return render_template('index.html')
@bp_dashboard.route('/users')
# @login_required
def users_page():
return render_template('users.html')
@bp_dashboard.route('/logs')
# @login_required
def logs_page():
return render_template('logs.html')
@bp_dashboard.route('/regions')
# @login_required
def regions_page():
return render_template('regions.html')
# Роуты для API
@bp_api.route('/users', methods=['GET'])
def get_users():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
session = Session()
query = text("""
SELECT w.chat_id, w.username, w.user_email ,s.region_id, s.disaster_only
FROM whitelist w
LEFT JOIN subscriptions s ON w.chat_id = s.chat_id AND s.active = 1
""")
users = session.execute(query).fetchall()
# Если users пустые, выводим сообщение в консоль
if not users:
print("No users found")
# Группируем подписки по chat_id
user_dict = {}
for u in users:
chat_id = u[0]
if chat_id not in user_dict:
disaster_only_text = "Только критические уведомления" if u[4] == 1 else "Все уведомления"
# is_blocked = "Заблокирован" if u[5] == 1 else "Активен"
user_dict[chat_id] = {
'id': u[0],
'username': u[1],
'email': u[2],
'subscriptions': [],
'disaster_only': disaster_only_text,
# 'status': is_blocked
}
if u[3]:
user_dict[chat_id]['subscriptions'].append(u[3])
users_list = list(user_dict.values())
total_users = len(users_list)
total_pages = (total_users + per_page - 1) // per_page
start = (page - 1) * per_page
end = start + per_page
users_page = users_list[start:end]
session.close()
return jsonify({
'users': users_page,
'total_users': total_users,
'total_pages': total_pages,
'current_page': page,
'per_page': per_page
})
@bp_api.route('/regions', methods=['GET', 'POST'])
def manage_regions():
session = Session()
if request.method == 'POST':
data = request.json
region_id = data.get('region_id')
name = data.get('name')
active = data.get('active', True)
region = Region(region_id=region_id, region_name=name, active=active)
session.add(region)
session.commit()
return jsonify({'status': 'success'})
regions = session.query(Region).all()
session.close()
return jsonify([{'region_id': r.region_id, 'name': r.region_name, 'active': r.active} for r in regions])
@bp_api.route('/regions/<int:region_id>', methods=['PUT', 'DELETE'])
def edit_region(region_id):
session = Session()
region = session.query(Region).filter_by(region_id=region_id).first()
if request.method == 'PUT':
data = request.json
region.region_name = data.get('name', region.region_name)
region.active = data.get('active', region.active)
session.commit()
session.close()
return jsonify({'status': 'updated'})
elif request.method == 'DELETE':
session.delete(region)
session.commit()
session.close()
return jsonify({'status': 'deleted'})
@bp_api.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
session = Session()
user = session.execute(text("SELECT * FROM whitelist WHERE chat_id = :id"), {'id': user_id}).fetchone()
session.close()
if not user:
return jsonify({'error': 'Пользователь не найден'}), 404
return jsonify({'id': user.chat_id, 'username': user.username, 'email': user.user_email, 'blocked': user.is_blocked})
# @bp_api.route('/users/<int:user_id>/block', methods=['POST'])
# def block_user(user_id):
# session = Session()
# session.execute(text("UPDATE whitelist SET is_blocked = False WHERE chat_id = :id"), {'id': user_id})
# session.commit()
# session.close()
# return jsonify({'status': 'updated'})
@bp_api.route('/users/<int:user_id>/block', methods=['POST'])
def block_user(user_id):
session = Session()
# Получаем текущий статус блокировки пользователя
result = session.execute(text("SELECT is_blocked FROM whitelist WHERE chat_id = :id"), {'id': user_id}).fetchone()
if result:
is_blocked = result[0] # Текущее значение блокировки
# Если пользователь заблокирован, разблокируем его, если разблокирован - блокируем
new_status = not is_blocked
# Обновляем статус блокировки в базе данных
session.execute(
text("UPDATE whitelist SET is_blocked = :new_status WHERE chat_id = :id"),
{'new_status': new_status, 'id': user_id}
)
session.commit()
session.close()
return jsonify({'status': 'updated', 'new_status': new_status})
else:
session.close()
return jsonify({'status': 'error', 'message': 'User not found'}), 404
@bp_api.route('/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
session = Session()
session.execute(text("DELETE FROM whitelist WHERE chat_id = :id"), {'id': user_id})
session.commit()
session.close()
return jsonify({'status': 'deleted'})

19
frontend/models.py Normal file
View File

@ -0,0 +1,19 @@
from utilities.database import db # Импортируем db из backend_flask.py
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
is_blocked = db.Column(db.Boolean, default=False)
actions = db.Column(db.String(500))
subscriptions = db.Column(db.String(500))
class Region(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), nullable=False)
active = db.Column(db.Boolean, default=True)
class Log(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), nullable=False)
action = db.Column(db.String(500), nullable=False)
timestamp = db.Column(db.DateTime, default=db.func.current_timestamp())

14
frontend/routes/auth.py Normal file
View File

@ -0,0 +1,14 @@
from flask import Blueprint, render_template, request, redirect, url_for
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
# Обработка логики авторизации
pass
return render_template('login.html')
@auth_bp.route('/')
def index():
return redirect(url_for('auth.login'))

9
frontend/routes/logs.py Normal file
View File

@ -0,0 +1,9 @@
from flask import Blueprint, render_template
from frontend.models import Log
logs_bp = Blueprint('logs', __name__)
@logs_bp.route('/logs')
def logs():
logs = Log.query.all()
return render_template('logs.html', logs=logs)

View File

@ -0,0 +1,9 @@
from flask import Blueprint, render_template
from frontend.models import Region
regions_bp = Blueprint('regions', __name__)
@regions_bp.route('/regions')
def regions():
regions = Region.query.all()
return render_template('regions.html', regions=regions)

9
frontend/routes/users.py Normal file
View File

@ -0,0 +1,9 @@
from flask import Blueprint, render_template
from frontend.models import User
users_bp = Blueprint('users', __name__)
@users_bp.route('/users')
def users():
user = User.query.all()
return render_template('users.html', users=user)

View File

@ -1,107 +0,0 @@
import sqlite3
import logging
from threading import Lock
db_lock = Lock()
# Инициализируем логгер
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) # Устанавливаем уровень логирования
class RegionAPI:
def __init__(self, db_path):
self.db_path = db_path
def add_region(self, region_id: int, region_name: str):
logger.info(f"Запрос на добавление региона: id={region_id}, name={region_name}")
# Проверка валидности region_id
if not str(region_id).isdigit():
logger.error(f"region_id {region_id} не является числом.")
return {"status": "failure", "message": "Region_id must be digit only"}
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
logger.debug(f"Проверка существования региона с id={region_id}")
cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,))
count = cursor.fetchone()[0]
if count == 0:
# Добавляем новый регион
cursor.execute('INSERT INTO regions (region_id, region_name, active) VALUES (?, ?, 1)',
(region_id, region_name))
conn.commit()
logger.info(f"Регион с id={region_id} успешно добавлен.")
return {"status": "success", "message": "Region added successfully"}
else:
logger.warning(f"Регион с id={region_id} уже существует.")
return {"status": "error", "message": "Region already exists"}
def remove_region(self, region_id):
logger.info(f"Запрос на удаление региона: id={region_id}")
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
logger.debug(f"Проверка существования региона с id={region_id}")
cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,))
count = cursor.fetchone()[0]
if count == 0:
logger.warning(f"Регион с id={region_id} не найден.")
return {"status": "error", "message": "Region not found"}
else:
cursor.execute('DELETE FROM regions WHERE region_id = ?', (region_id,))
conn.commit()
logger.info(f"Регион с id={region_id} успешно удалён.")
return {"status": "success", "message": "Region removed successfully"}
def get_regions(self):
logger.info("Запрос на получение списка регионов.")
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
logger.debug("Извлечение данных из таблицы regions.")
cursor.execute('SELECT region_id, region_name, active FROM regions')
regions = cursor.fetchall()
logger.info(f"Получено {len(regions)} регионов.")
return [{"region_id": r[0], "region_name": r[1], "regions_active": r[2]} for r in regions]
def change_region_status(self, region_id, active):
logger.info(f"Запрос на изменение статуса региона: id={region_id}, статус={active}")
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
logger.debug(f"Проверка существования региона с id={region_id}")
cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,))
count = cursor.fetchone()[0]
if count == 0:
logger.warning(f"Регион с id={region_id} не найден.")
return {"status": "error", "message": "Region not found"}
else:
cursor.execute('UPDATE regions SET active = ? WHERE region_id = ?', (active, region_id))
conn.commit()
logger.info(f"Статус региона с id={region_id} успешно изменён.")
return {"status": "success", "message": "Region status updated successfully"}
def update_region_status(self, region_id, active):
logger.info(f"Запрос на обновление статуса региона: id={region_id}, активность={active}")
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
# Проверяем существование региона
logger.debug(f"Проверка существования региона с id={region_id}")
cursor.execute("SELECT region_name FROM regions WHERE region_id = ?", (region_id,))
result = cursor.fetchone()
if not result:
logger.warning(f"Регион с id={region_id} не найден.")
return {"status": "error", "message": "Регион не найден"}
# Обновляем статус активности региона
cursor.execute("UPDATE regions SET active = ? WHERE region_id = ?", (int(active), region_id))
conn.commit()
action = "Активирован" if active else "Отключён"
logger.info(f"Регион с id={region_id} {action}.")
return {"status": "success", "message": f"Регион {region_id} {action}"}

View File

@ -11,10 +11,16 @@ click==8.1.8
colorama==0.4.6 colorama==0.4.6
exceptiongroup==1.2.2 exceptiongroup==1.2.2
Flask==3.1.0 Flask==3.1.0
flask-ldap3-login==1.0.2
Flask-Login==0.6.3
Flask-SQLAlchemy==3.1.1
Flask-WTF==1.2.2
frozenlist==1.5.0 frozenlist==1.5.0
greenlet==3.1.1
idna==3.10 idna==3.10
itsdangerous==2.2.0 itsdangerous==2.2.0
Jinja2==3.1.5 Jinja2==3.1.5
ldap3==2.9.1
MarkupSafe==3.0.2 MarkupSafe==3.0.2
multidict==6.1.0 multidict==6.1.0
packaging==24.2 packaging==24.2
@ -22,13 +28,17 @@ pamqp==3.3.0
pika==1.3.2 pika==1.3.2
pika-stubs==0.1.3 pika-stubs==0.1.3
propcache==0.2.1 propcache==0.2.1
pyasn1==0.6.1
pyTelegramBotAPI==4.26.0 pyTelegramBotAPI==4.26.0
python-dotenv==1.0.1 python-dotenv==1.0.1
pytz==2025.1 pytz==2025.1
pyzabbix==1.3.1 pyzabbix==1.3.1
requests==2.32.3 requests==2.32.3
schedule==1.2.2 schedule==1.2.2
SQLAlchemy==2.0.38
telebot==0.0.5 telebot==0.0.5
typing_extensions==4.12.2
urllib3==2.3.0 urllib3==2.3.0
Werkzeug==3.1.3 Werkzeug==3.1.3
WTForms==3.2.1
yarl==1.18.3 yarl==1.18.3

View File

@ -1,25 +1 @@
/* Добавим выравнивание и отступы для кнопок управления */
.table-hover tbody tr td {
vertical-align: middle;
}
.table-hover tbody tr td .btn-manage {
margin-left: 20px; /* Отступ кнопки от названия региона */
}
th, td {
text-align: left; /* Выровнять содержимое слева */
}
.table-hover tbody tr {
height: 50px; /* Сделать строки таблицы выше для лучшего визуального эффекта */
}
.modal-footer .btn {
margin-left: 10px;
}
/* Стили для пагинации */
.d-flex .btn {
margin: 0 5px;
}

View File

@ -1,55 +1,173 @@
$(document).ready(function() { let currentPage = 1;
// Получаем список сотрудников let totalPages = 1;
$.getJSON('/telezab/users/get', function(data) { const perPage = 20;
var userList = $('#user-list');
userList.empty(); // Функция загрузки пользователей
data.forEach(function(user) { function loadUsers(page) {
var email = user.email; if (page < 1 || page > totalPages) return;
var name = email.split('@')[0].replace(/\./g, ' ').replace(/\b\w/g, char => char.toUpperCase()); currentPage = page;
var listItem = $('<li class="list-group-item"></li>').text(name).data('user', user);
userList.append(listItem); fetch(`/telezab/rest/api/users?page=${currentPage}&per_page=${perPage}`)
.then(response => response.json())
.then(data => {
totalPages = data.total_pages;
updateUsersTable(data.users);
updatePagination(data.current_page, data.total_pages);
})
.catch(error => {
console.error('Error fetching users:', error);
});
}
// Функция обновления таблицы пользователей
function updateUsersTable(users) {
const tableBody = document.getElementById('users-table').querySelector('tbody');
tableBody.innerHTML = '';
users.forEach(user => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${user.id}</td>
<td>${user.username}</td>
<td>${user.email}</td>
<td>${user.subscriptions.join(', ') || 'Нет подписок'}</td>
<td>${user.disaster_only}</td>
<td>${user.status}</td>
<td><button class="btn btn-primary editUserBtn" data-id="${user.id}">Редактировать</button></td>
`;
tableBody.appendChild(row);
});
setupEditButtons();
}
// Функция для обработки кнопок "Редактировать"
function setupEditButtons() {
document.querySelectorAll(".editUserBtn").forEach(button => {
button.addEventListener("click", function () {
const userId = this.dataset.id;
openUserModal(userId);
}); });
}); });
}
// Обработчик кликов по пользователям // Функция открытия модального окна
$('#user-list').on('click', '.list-group-item', function() { function openUserModal(userId) {
// Удаляем активный класс у всех элементов списка fetch(`/telezab/rest/api/users/${userId}`)
$('.list-group-item').removeClass('active'); .then(response => response.json())
.then(data => {
document.getElementById("userId").innerText = data.id;
document.getElementById("username").innerText = data.username;
document.getElementById("userEmail").innerText = data.email;
// Добавляем активный класс к выбранному элементу const blockBtn = document.getElementById("toggleBlockUser");
$(this).addClass('active'); blockBtn.innerText = data.blocked ? "Разблокировать" : "Заблокировать";
blockBtn.onclick = function () {
toggleUserBlock(userId);
};
var user = $(this).data('user'); document.getElementById("viewUserEvents").onclick = function () {
$('#user-info').removeClass('d-none'); viewUserEvents(userId);
$('#user-name').text(user.email.split('@')[0].replace(/\./g, ' ').replace(/\b\w/g, char => char.toUpperCase())); };
// Отображаем регионы в одну строку document.getElementById("deleteUser").onclick = function () {
var regions = $('#user-regions'); deleteUser(userId);
regions.empty(); };
if (user.subscriptions) {
var subscriptions = user.subscriptions.split(',').map(function(sub) { return sub.trim(); });
if (subscriptions.length > 0) {
regions.text(subscriptions.join(', '));
} else {
regions.text('Нет подписок');
}
} else {
regions.text('Нет подписок');
}
// Отображаем действия // Использование Bootstrap для показа модального окна
var events = $('#user-events'); var myModal = new bootstrap.Modal(document.getElementById('userModal'), {
events.empty(); keyboard: false
if (user.events && user.events.length > 0) {
user.events.forEach(function(event) {
var eventText = event.type;
if (event.region) {
eventText += ' (Регион: ' + event.region + ')';
}
events.append('<div><strong>' + event.date + '</strong> - ' + eventText + '</div>');
}); });
} else { myModal.show(); // Открытие модального окна
events.append('<div>Нет действий</div>'); })
} .catch(error => {
}); console.error('Error fetching user data:', error);
});
}
// Обработчик закрытия модального окна
document.querySelector(".btn-close").addEventListener("click", function () {
var myModal = new bootstrap.Modal(document.getElementById('userModal'));
myModal.hide(); // Закрытие модального окна
});
// Функция для блокировки/разблокировки пользователя
function toggleUserBlock(userId) {
fetch(`/telezab/rest/api/users/${userId}/block`, { method: "POST" })
.then(() => {
alert("Статус пользователя изменён");
// Скрыть модальное окно с помощью Bootstrap (это не отменяет событий закрытия)
var myModal = new bootstrap.Modal(document.getElementById('userModal'));
myModal.hide(); // Закрытие модального окна
loadUsers(currentPage); // Перезагрузите список пользователей
})
.catch(error => {
console.error("Ошибка при изменении статуса пользователя:", error);
});
}
// Функция просмотра действий пользователя
function viewUserEvents(userId) {
window.location.href = `/telezab/rest/api/users/${userId}/events`;
}
// Функция для удаления пользователя
function deleteUser(userId) {
if (confirm("Вы уверены, что хотите удалить пользователя?")) {
fetch(`/telezab/rest/api/users/${userId}`, { method: "DELETE" })
.then(() => {
alert("Пользователь удалён");
var myModal = new bootstrap.Modal(document.getElementById('userModal'));
myModal.hide();
loadUsers(currentPage); // Перезагрузите список пользователей
});
}
}
window.onclick = function (event) {
if (event.target === document.getElementById("userModal")) {
$('#userModal').modal('hide'); // Закрываем модальное окно с помощью Bootstrap
}
};
// Функция обновления пагинации
function updatePagination(currentPage, totalPages) {
const paginationContainer = document.getElementById('pagination');
paginationContainer.innerHTML = '';
const prevButton = document.createElement('li');
prevButton.classList.add('page-item');
prevButton.classList.toggle('disabled', currentPage === 1);
prevButton.innerHTML = `<a class="page-link" href="#" aria-label="Previous" onclick="loadUsers(${currentPage - 1})">&laquo;</a>`;
paginationContainer.appendChild(prevButton);
for (let page = 1; page <= totalPages; page++) {
const pageItem = document.createElement('li');
pageItem.classList.add('page-item');
pageItem.classList.toggle('active', page === currentPage);
const pageLink = document.createElement('a');
pageLink.classList.add('page-link');
pageLink.href = "#";
pageLink.textContent = page;
pageLink.onclick = () => loadUsers(page);
pageItem.appendChild(pageLink);
paginationContainer.appendChild(pageItem);
}
const nextButton = document.createElement('li');
nextButton.classList.add('page-item');
nextButton.classList.toggle('disabled', currentPage === totalPages);
nextButton.innerHTML = `<a class="page-link" href="#" aria-label="Next" onclick="loadUsers(${currentPage + 1})">&raquo;</a>`;
paginationContainer.appendChild(nextButton);
}
// Запуск загрузки данных
document.addEventListener("DOMContentLoaded", () => {
loadUsers(currentPage);
}); });

View File

@ -2,11 +2,9 @@ import asyncio
import logging import logging
import sqlite3 import sqlite3
from threading import Thread from threading import Thread
import telebot import telebot
from pyzabbix import ZabbixAPI from pyzabbix import ZabbixAPI
from telebot import types from telebot import types
import backend_bot import backend_bot
import bot_database import bot_database
from backend_flask import app from backend_flask import app
@ -14,13 +12,14 @@ from backend_locks import bot
from backend_locks import db_lock from backend_locks import db_lock
from backend_zabbix import get_triggers_for_group, get_triggers_for_all_groups from backend_zabbix import get_triggers_for_group, get_triggers_for_all_groups
from config import * from config import *
from log_manager import LogManager from utilities.log_manager import LogManager
from rabbitmq import consume_from_queue from utilities.rabbitmq import consume_from_queue
from user_state_manager import UserStateManager from utilities.telegram_utilities import show_main_menu, show_settings_menu
from utils import show_main_menu, show_settings_menu from utilities.user_state_manager import UserStateManager
# Инициализируем класс UserStateManager # Инициализируем класс UserStateManager
user_state_manager = UserStateManager() state = UserStateManager()
# Инициализация LogManager # Инициализация LogManager
log_manager = LogManager(log_dir='logs', retention_days=30) log_manager = LogManager(log_dir='logs', retention_days=30)
@ -31,12 +30,6 @@ telebot.logger = logging.getLogger('telebot')
# Важно: вызов schedule_log_rotation для планировки ротации и архивации логов # Важно: вызов schedule_log_rotation для планировки ротации и архивации логов
log_manager.schedule_log_rotation() log_manager.schedule_log_rotation()
# # Lock for database operations
# db_lock = Lock()
# 25 messages per second
# Handle /help command to provide instructions # Handle /help command to provide instructions
@bot.message_handler(commands=['help']) @bot.message_handler(commands=['help'])
def handle_help(message): def handle_help(message):
@ -92,7 +85,7 @@ def handle_menu_selection(message):
backend_bot.bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.") backend_bot.bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.")
return return
# Получаем текущее состояние пользователя # Получаем текущее состояние пользователя
current_state = user_state_manager.get_state(chat_id) current_state = state.get_state(chat_id)
# Обработка команд в зависимости от состояния # Обработка команд в зависимости от состояния
if current_state == "MAIN_MENU": if current_state == "MAIN_MENU":
backend_bot.handle_main_menu(message, chat_id, text) backend_bot.handle_main_menu(message, chat_id, text)
@ -116,7 +109,7 @@ def handle_cancel_action(call):
backend_bot.bot.clear_step_handler_by_chat_id(chat_id) backend_bot.bot.clear_step_handler_by_chat_id(chat_id)
backend_bot.bot.send_message(chat_id, f"Действие отменено") backend_bot.bot.send_message(chat_id, f"Действие отменено")
backend_bot.bot.edit_message_reply_markup(chat_id, message_id, reply_markup=None) backend_bot.bot.edit_message_reply_markup(chat_id, message_id, reply_markup=None)
user_state_manager.set_state(chat_id, "SETTINGS_MENU") state.set_state(chat_id, "SETTINGS_MENU")
show_settings_menu(chat_id) show_settings_menu(chat_id)
return return
@ -128,7 +121,7 @@ def handle_cancel_active_triggers(call):
backend_bot.bot.clear_step_handler_by_chat_id(chat_id) backend_bot.bot.clear_step_handler_by_chat_id(chat_id)
backend_bot.bot.send_message(chat_id, f"Действие отменено") backend_bot.bot.send_message(chat_id, f"Действие отменено")
backend_bot.bot.edit_message_reply_markup(chat_id, message_id, reply_markup=None) backend_bot.bot.edit_message_reply_markup(chat_id, message_id, reply_markup=None)
user_state_manager.set_state(chat_id, "MAIN_MENU") state.set_state(chat_id, "MAIN_MENU")
show_main_menu(chat_id) show_main_menu(chat_id)
return return
@ -229,7 +222,7 @@ def handle_notification_mode_selection(call):
telebot.logger.info(f"Notification mode for user ({chat_id}) updated to: {mode_text}") telebot.logger.info(f"Notification mode for user ({chat_id}) updated to: {mode_text}")
# Логируем изменение состояния пользователя # Логируем изменение состояния пользователя
user_state_manager.set_state(chat_id, "SETTINGS_MENU") state.set_state(chat_id, "SETTINGS_MENU")
telebot.logger.debug(f"User state for {chat_id} set to SETTINGS_MENU.") telebot.logger.debug(f"User state for {chat_id} set to SETTINGS_MENU.")
# Показываем меню настроек # Показываем меню настроек
@ -381,7 +374,6 @@ def run_flask():
def main(): def main():
# Инициализация базы данных # Инициализация базы данных
bot_database.init_db() bot_database.init_db()
# Запуск Flask и бота в отдельных потоках # Запуск Flask и бота в отдельных потоках
Thread(target=run_flask, daemon=True).start() Thread(target=run_flask, daemon=True).start()
Thread(target=run_polling, daemon=True).start() Thread(target=run_polling, daemon=True).start()

44
templates/base.html Normal file
View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Dashboard{% endblock %}</title>
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
</head>
<body>
<!-- Навигационное меню -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Dashboard</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'dashboard.dashboard' %}active{% endif %}" href="{{ url_for('dashboard.dashboard') }}">Главная</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'dashboard.users_page' %}active{% endif %}" href="{{ url_for('dashboard.users_page') }}">Пользователи</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'dashboard.regions_page' %}active{% endif %}" href="{{ url_for('dashboard.regions_page') }}">Регионы</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'dashboard.logs_page' %}active{% endif %}" href="{{ url_for('dashboard.logs_page') }}">Логи</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- Основной контент -->
<div class="container mt-4">
{% block content %}{% endblock %}
</div>
<script src="{{ url_for('static', filename='js/jquery-3.6.0.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
</body>
</html>

145
templates/dashboard.html Normal file
View File

@ -0,0 +1,145 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<h1 class="mb-4">Панель управления</h1>
<!-- Таблица пользователей -->
<h2>Пользователи</h2>
<table id="users-table" class="table table-bordered table-striped">
<thead class="thead-dark">
<tr>
<th>ID</th>
<th>Имя</th>
<th>Email</th>
<th>Подписки</th>
<th>Режим</th>
</tr>
</thead>
<tbody>
<!-- Данные загружаются через JS -->
</tbody>
</table>
<!-- Контейнер пагинации -->
<nav aria-label="Page navigation example">
<ul class="pagination" id="pagination">
<!-- Кнопка предыдущей страницы -->
<li class="page-item">
<a class="page-link" href="#" aria-label="Previous" onclick="loadUsers(currentPage - 1)">
<span aria-hidden="true">&laquo;</span>
<span class="sr-only">Previous</span>
</a>
</li>
<!-- Номера страниц (будет динамически генерироваться) -->
<!-- Пример -->
<!-- <li class="page-item"><a class="page-link" href="#">1</a></li> -->
<!-- Кнопка следующей страницы -->
<li class="page-item">
<a class="page-link" href="#" aria-label="Next" onclick="loadUsers(currentPage + 1)">
<span aria-hidden="true">&raquo;</span>
<span class="sr-only">Next</span>
</a>
</li>
</ul>
</nav>
</div>
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
{#<script src="{{ url_for('static', filename='js/regions.js') }}"></script>#}
<script>
let currentPage = 1; // Переменная для текущей страницы
let totalPages = 1; // Переменная для общего количества страниц (инициализируем значением 1 по умолчанию)
const perPage = 20; // Количество элементов на странице
// Функция для загрузки данных
function loadUsers(page) {
if (page < 1 || page > totalPages) return; // Проверка на допустимые страницы
currentPage = page; // Обновляем текущую страницу
// Получаем данные с сервера
fetch(`/telezab/dashboard/users?page=${currentPage}&per_page=${perPage}`)
.then(response => response.json())
.then(data => {
totalPages = data.total_pages; // Обновляем totalPages из ответа сервера
updateUsersTable(data.users); // Обновляем таблицу пользователей
updatePagination(data.current_page, data.total_pages); // Обновляем пагинацию
})
.catch(error => {
console.error('Error fetching users:', error);
});
}
// Функция для обновления таблицы пользователей
function updateUsersTable(users) {
const tableBody = document.getElementById('users-table').querySelector('tbody');
tableBody.innerHTML = ''; // Очищаем таблицу
users.forEach(user => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${user.id}</td>
<td>${user.username}</td>
<td>${user.email}</td>
<td>${user.subscriptions.join(', ') || 'Нет подписок'}</td>
<td>${user.disaster_only}</td>
`;
tableBody.appendChild(row);
});
}
// Функция для обновления пагинации
function updatePagination(currentPage, totalPages) {
const paginationContainer = document.getElementById('pagination');
paginationContainer.innerHTML = ''; // Очищаем пагинацию
// Кнопка "Предыдущая" (слева)
const prevButton = document.createElement('li');
prevButton.classList.add('page-item');
prevButton.classList.toggle('disabled', currentPage === 1); // Отключить кнопку если текущая страница - 1
prevButton.innerHTML = `<a class="page-link" href="#" aria-label="Previous" onclick="loadUsers(${currentPage - 1})">
<span aria-hidden="true">&laquo;</span>
<span class="sr-only">Previous</span>
</a>`;
paginationContainer.appendChild(prevButton);
// Генерация кнопок с номерами страниц
for (let page = 1; page <= totalPages; page++) {
const pageItem = document.createElement('li');
pageItem.classList.add('page-item');
pageItem.classList.toggle('active', page === currentPage); // Выделяем текущую страницу
const pageLink = document.createElement('a');
pageLink.classList.add('page-link');
pageLink.href = "#";
pageLink.textContent = page;
pageLink.onclick = () => loadUsers(page);
pageItem.appendChild(pageLink);
paginationContainer.appendChild(pageItem);
}
// Кнопка "Следующая" (справа)
const nextButton = document.createElement('li');
nextButton.classList.add('page-item');
nextButton.classList.toggle('disabled', currentPage === totalPages); // Отключить кнопку если текущая страница - последняя
nextButton.innerHTML = `<a class="page-link" href="#" aria-label="Next" onclick="loadUsers(${currentPage + 1})">
<span aria-hidden="true">&raquo;</span>
<span class="sr-only">Next</span>
</a>`;
paginationContainer.appendChild(nextButton);
}
// Инициализация загрузки пользователей на первой странице
loadUsers(currentPage);
</script>
</body>
</html>

View File

@ -1,10 +1,55 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="en">
<head> {% block title %}Dashboard Home{% endblock %}
<meta charset="UTF-8">
<title>Index of WebUI for Telezab</title> {% block content %}
</head> <h1 class="text-center mb-4">Добро пожаловать в Dashboard</h1>
<body> <p class="text-center">Выберите один из разделов:</p>
THIS IS WEBUI FOR TELEZAB BOT
</body> <div class="row">
</html> <!-- Карточка пользователей -->
<div class="col-md-4">
<div class="card shadow-sm">
<div class="card-body text-center">
<h5 class="card-title">Пользователи</h5>
<p class="card-text">Просмотр и управление пользователями.</p>
<a href="{{ url_for('dashboard.users_page') }}" class="btn btn-primary">Перейти</a>
</div>
</div>
</div>
<!-- Карточка регионов -->
<div class="col-md-4">
<div class="card shadow-sm">
<div class="card-body text-center">
<h5 class="card-title">Регионы</h5>
<p class="card-text">Просмотр и настройка регионов.</p>
<a href="{{ url_for('dashboard.regions_page') }}" class="btn btn-primary">Перейти</a>
</div>
</div>
</div>
<!-- Карточка логов -->
<div class="col-md-4">
<div class="card shadow-sm">
<div class="card-body text-center">
<h5 class="card-title">Логи</h5>
<p class="card-text">Просмотр событий и логов.</p>
<a href="{{ url_for('dashboard.logs_page') }}" class="btn btn-primary">Перейти</a>
</div>
</div>
</div>
<!-- Карточка Teams Chat -->
<div class="col-md-4">
<div class="card shadow-sm">
<div class="card-body text-center">
<h5 class="card-title">Создание чатов Teams</h5>
<p class="card-text">Создание чатов в VK Teams в случае аварий</p>
<a href="{{ url_for('dashboard.logs_page') }}" class="btn btn-primary">Перейти</a>
</div>
</div>
</div>
</div>
{% endblock %}

20
templates/login.html Normal file
View File

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<h2>Login</h2>
<form method="POST" action="{{ url_for('login') }}">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
</div>
</div>
{% endblock %}

10
templates/logs.html Normal file
View File

@ -0,0 +1,10 @@
<!-- templates/logs.html -->
{% extends "base.html" %}
{% block title %}Логи{% endblock %}
{% block content %}
<h2>Логи</h2>
<pre id="logs-container"></pre>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/logs.js') }}"></script>
{% endblock %}

View File

@ -1,53 +1,20 @@
<!DOCTYPE html> <!-- templates/regions.html -->
<html lang="en"> {% extends "base.html" %}
<head> {% block title %}Регионы{% endblock %}
<meta charset="UTF-8"> {% block content %}
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <h2>Регионы</h2>
<title>Region Management</title> <table class="table table-bordered">
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}"> <thead>
<link rel="stylesheet" href="{{ url_for('static', filename='css/regions.css') }}"> <tr>
</head> <th>ID</th>
<body> <th>Название</th>
<div class="container mt-4"> <th>Активен</th>
<div class="row"> </tr>
<!-- Левый контейнер со списком регионов --> </thead>
<div class="col-md-6"> <tbody id="regions-table">
<h3>Список Регионов</h3> </tbody>
<div class="list-group" id="region-list"> </table>
<!-- Список регионов будет добавляться сюда --> {% endblock %}
</div> {% block scripts %}
<!-- Пагинация -->
<nav aria-label="Page navigation">
<ul class="pagination" id="pagination">
<!-- Кнопки пагинации будут добавляться сюда -->
</ul>
</nav>
</div>
<!-- Правый контейнер с информацией о пользователях -->
<div class="col-md-6">
<h3>Пользователи</h3>
<div id="curent-region">
</div>
<div id="user-info">
<!-- Информация о пользователях будет добавляться сюда -->
</div>
<ul id="user-list">
<!-- Список пользователей будет добавляться сюда -->
</ul>
<!-- Кнопки управления регионами -->
<div class="d-flex justify-content-end mt-3">
<button type="button" class="btn btn-danger me-2" id="delete-region">Удалить регион</button>
<button type="button" class="btn btn-warning me-2" id="disable-region">Отключить регион</button>
<button type="button" class="btn btn-success" id="activate-region">Активировать регион</button>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/regions.js') }}"></script> <script src="{{ url_for('static', filename='js/regions.js') }}"></script>
</body> {% endblock %}
</html>

View File

@ -1,37 +1,64 @@
<!DOCTYPE html> {% extends 'base.html' %}
<html lang="ru">
<head> {% block content %}
<meta charset="UTF-8"> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="{{ url_for('static', filename='css/users.css') }}">
<title>Сотрудники</title> <title>Пользователи</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}"> </head>
<link rel="stylesheet" href="{{ url_for('static', filename='css/users.css') }}"> <div class="container">
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script> <h1>Пользователи</h1>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/users.js') }}"></script> <!-- Таблица пользователей -->
</head> <table class="table" id="users-table">
<body> <thead>
<div class="container mt-4"> <tr>
<div class="row"> <th>Chat ID</th>
<div class="col-md-4"> <th>Telegram ID</th>
<ul id="user-list" class="list-group"> <th>Email</th>
<!-- Список сотрудников будет вставлен сюда --> <th>Подписки</th>
</ul> <th>Тип уведомлений</th>
</div> <th>Статус</th>
<div class="col-md-8"> <th>Действия</th>
<div id="user-info" class="d-none"> </tr>
<h3 id="user-name"></h3> </thead>
<h5>Подписки на регионы</h5> <tbody>
<ul id="user-regions" class="list-group"> <!-- Данные будут загружаться сюда динамически -->
<!-- Подписки на регионы будут вставлены сюда --> </tbody>
</ul> </table>
<h5>Действия</h5>
<div id="user-events" class="overflow-auto" style="max-height: 300px;"> <!-- Пагинация -->
<!-- Действия будут вставлены сюда --> <nav>
</div> <ul class="pagination" id="pagination">
<!-- Страницы будут генерироваться динамически -->
</ul>
</nav>
</div>
<!-- Модальное окно -->
<div id="userModal" class="modal fade" tabindex="-1" aria-labelledby="userModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="userModalLabel">Редактирование пользователя</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Chat ID: <span id="userId"></span></p>
<p>Telegram ID: <span id="username"></span></p>
<p>Email: <span id="userEmail"></span></p>
</div>
<div class="modal-footer">
<button id="toggleBlockUser" class="btn btn-warning">Заблокировать</button>
<button id="viewUserEvents" class="btn btn-info">Посмотреть действия</button>
<button id="deleteUser" class="btn btn-danger">Удалить</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</body>
</html>
<!-- Подключаем внешний JavaScript файл -->
<script src="{{ url_for('static', filename='js/users.js') }}"></script>
{% endblock %}

37
templates/users_old.html Normal file
View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Сотрудники</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/users.css') }}">
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/users.js') }}"></script>
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-md-4">
<ul id="user-list" class="list-group">
<!-- Список сотрудников будет вставлен сюда -->
</ul>
</div>
<div class="col-md-8">
<div id="user-info" class="d-none">
<h3 id="user-name"></h3>
<h5>Подписки на регионы</h5>
<ul id="user-regions" class="list-group">
<!-- Подписки на регионы будут вставлены сюда -->
</ul>
<h5>Действия</h5>
<div id="user-events" class="overflow-auto" style="max-height: 300px;">
<!-- Действия будут вставлены сюда -->
</div>
</div>
</div>
</div>
</div>
</body>
</html>

3
utilities/database.py Normal file
View File

@ -0,0 +1,3 @@
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy() # Создаем экземпляр SQLAlchemy

View File

@ -62,12 +62,12 @@ class LogManager:
}, },
'handlers': { 'handlers': {
'telebot_console': { 'telebot_console': {
'class': 'log_manager.UTF8StreamHandler', 'class': 'utilities.log_manager.UTF8StreamHandler',
'stream': 'ext://sys.stdout', 'stream': 'ext://sys.stdout',
'formatter': 'default', 'formatter': 'default',
}, },
'flask_console': { 'flask_console': {
'class': 'log_manager.UTF8StreamHandler', 'class': 'utilities.log_manager.UTF8StreamHandler',
'stream': 'ext://sys.stdout', 'stream': 'ext://sys.stdout',
'formatter': 'werkzeug', 'formatter': 'werkzeug',
}, },

View File

@ -91,10 +91,10 @@ def escape_telegram_chars(text):
def show_main_menu(chat_id): def show_main_menu(chat_id):
markup = telebot.types.ReplyKeyboardMarkup(one_time_keyboard=True, resize_keyboard=True) markup = telebot.types.ReplyKeyboardMarkup(one_time_keyboard=True, resize_keyboard=True)
if bot_database.is_whitelisted(chat_id): if bot_database.is_whitelisted(chat_id):
telezab.user_state_manager.set_state(chat_id, "MAIN_MENU") telezab.state.set_state(chat_id, "MAIN_MENU")
markup.add('Настройки', 'Помощь', 'Активные события') markup.add('Настройки', 'Помощь', 'Активные события')
else: else:
telezab.user_state_manager.set_state(chat_id, "REGISTRATION") telezab.state.set_state(chat_id, "REGISTRATION")
markup.add('Регистрация') markup.add('Регистрация')
backend_bot.bot.send_message(chat_id, "Выберите действие:", reply_markup=markup) backend_bot.bot.send_message(chat_id, "Выберите действие:", reply_markup=markup)
@ -109,7 +109,7 @@ def create_settings_keyboard(chat_id, admins_list):
def show_settings_menu(chat_id): def show_settings_menu(chat_id):
if not bot_database.is_whitelisted(chat_id): if not bot_database.is_whitelisted(chat_id):
telezab.user_state_manager.set_state(chat_id, "REGISTRATION") telezab.state.set_state(chat_id, "REGISTRATION")
backend_bot.bot.send_message(chat_id, "Вы неавторизованы для использования этого бота") backend_bot.bot.send_message(chat_id, "Вы неавторизованы для использования этого бота")
return return
admins_list = bot_database.get_admins() admins_list = bot_database.get_admins()

View File

@ -1,18 +0,0 @@
from jinja2 import TemplateNotFound
from flask import Blueprint, render_template, jsonify, abort
webui = Blueprint('webui', __name__, url_prefix='/telezab')
@webui.route('/', defaults={'index'})
def index():
try:
return render_template(index.html)
except TemplateNotFound:
abort(404)
@webui.route('/data')
def get_data():
return jsonify({"message": "Данные из frontend!"})

View File

@ -1,13 +0,0 @@
from flask import Blueprint, jsonify
webui = Blueprint('webui', __name__, url_prefix='/telezab')
@webui.route("/heartbeat")
def heartbeat():
return jsonify({"status": "healthy"})
@webui.route('/', defaults={'path': ''})
@webui.route('/<path:path>')
def catch_all():
return webui.send_static_file("index.html")