Compare commits
No commits in common. "52e31864b30fea16c57e5553d2677c001d29df81" and "b94e8d472459f52cdb615ad4901839b89656242c" have entirely different histories.
52e31864b3
...
b94e8d4724
@ -1,82 +0,0 @@
|
||||
import logging
|
||||
from flask import Flask, request, jsonify, redirect, url_for, session
|
||||
|
||||
from app.extensions.db import db
|
||||
from app.extensions.audit_logger import AuditLogger
|
||||
from app.models import *
|
||||
from app.models.user import User
|
||||
from app.routes import register_blueprints
|
||||
from app.extensions.auth_ext import init_auth, login_manager
|
||||
|
||||
import config
|
||||
from app.routes.dashboard import dashboard_bp
|
||||
# from backend.api import bp_api
|
||||
|
||||
from config import TZ
|
||||
|
||||
|
||||
# noinspection SpellCheckingInspection
|
||||
def create_app():
|
||||
app = Flask(__name__, static_url_path='/telezab/static', template_folder='templates')
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = config.SQLALCHEMY_DATABASE_URI
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['SECRET_KEY'] = config.SECRET_KEY # Замените на надежный секретный ключ
|
||||
app.config['SESSION_COOKIE_SECURE'] = config.SESSION_COOKIE_SECURE # Убедитесь, что установлено значение True
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = config.SESSION_COOKIE_HTTPONLY # Убедитесь, что установлено значение True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = config.SESSION_COOKIE_SAMESITE
|
||||
app.config['SESSION_REFRESH_EACH_REQUEST'] = False
|
||||
app.config['PERMANENT_SESSION_LIFETIME'] = config.PERMANENT_SESSION_LIFETIME
|
||||
app.config['SESSION_COOKIE_MAX_AGE'] = 3600
|
||||
app.config['TIMEZONE'] = TZ
|
||||
|
||||
# Инициализация расширений
|
||||
db.init_app(app)
|
||||
login_manager.init_app(app)
|
||||
init_auth(app)
|
||||
|
||||
# Инициализация AuditLogger с передачей db.session
|
||||
app.audit_logger = AuditLogger(db.session)
|
||||
|
||||
# Регистрируем блюпринты
|
||||
register_blueprints(app)
|
||||
|
||||
# Создаем таблицы (если нужно)
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
|
||||
|
||||
|
||||
@login_manager.unauthorized_handler
|
||||
def unauthorized():
|
||||
logging.debug("Unauthorized access detected")
|
||||
if request.path.startswith('/telezab/rest/api'):
|
||||
return jsonify({'error': 'Не авторизован'}), 401
|
||||
else:
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
user_data = session.get('user_data', {})
|
||||
|
||||
display_name = user_data.get('display_name')
|
||||
if not display_name:
|
||||
display_name = " ".join(filter(None, [
|
||||
user_data.get('user_surname'),
|
||||
user_data.get('user_name'),
|
||||
user_data.get('user_middle_name')
|
||||
]))
|
||||
|
||||
return User(
|
||||
user_id,
|
||||
user_name=user_data.get('user_name'),
|
||||
user_surname=user_data.get('user_surname'),
|
||||
user_middle_name=user_data.get('user_middle_name'),
|
||||
display_name=display_name,
|
||||
email=user_data.get('email')
|
||||
)
|
||||
|
||||
return app
|
||||
|
||||
app = create_app()
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
import os
|
||||
|
||||
#Дебаг режим
|
||||
DEV = os.getenv('DEV')
|
||||
#Токены и URL'ы
|
||||
BOT_TOKEN = os.getenv('TELEGRAM_TOKEN')
|
||||
ZABBIX_API_TOKEN = os.getenv('ZABBIX_API_TOKEN')
|
||||
ZABBIX_URL = os.getenv('ZABBIX_URL')
|
||||
SUPPORT_EMAIL = "shiftsupport-rtmis@rtmis.ru"
|
||||
HELP_URL = "https://confluence.is-mis.ru/pages/viewpage.action?pageId=416785183"
|
||||
DB_PATH = 'db/telezab.db'
|
||||
|
||||
RABBITMQ_HOST = os.getenv('RABBITMQ_HOST')
|
||||
RABBITMQ_LOGIN = os.getenv('RABBITMQ_LOGIN')
|
||||
RABBITMQ_PASS = os.getenv('RABBITMQ_PASS')
|
||||
RABBITMQ_QUEUE = 'telegram_notifications'
|
||||
RABBITMQ_URL_FULL = f"amqp://{RABBITMQ_LOGIN}:{RABBITMQ_PASS}@{RABBITMQ_HOST}/"
|
||||
@ -1,15 +0,0 @@
|
||||
# app/bot/handlers/help.py
|
||||
from telebot.types import Message
|
||||
from app.bot.config import HELP_URL
|
||||
|
||||
def register_handlers(bot):
|
||||
@bot.message_handler(commands=['help'])
|
||||
@bot.message_handler(func=lambda msg: msg.text == "Помощь")
|
||||
def handle_help(message: Message):
|
||||
help_text = (
|
||||
'<b>/start</b> - Показать меню бота\n'
|
||||
'<b>Настройки</b> - Перейти в режим настроек и управления подписками\n'
|
||||
'<b>Активные события</b> - Получение всех нерешённых событий мониторинга по выбранным сервисам выбранного региона\n'
|
||||
f'<b>Помощь</b> - <a href="{HELP_URL}">Описание всех возможностей бота</a>'
|
||||
)
|
||||
bot.send_message(message.chat.id, help_text, parse_mode="HTML")
|
||||
@ -1,13 +0,0 @@
|
||||
# app/bot/handlers/main_menu.py
|
||||
from telebot.types import Message
|
||||
from app.bot.keyboards.settings_menu import get_settings_menu
|
||||
|
||||
def register_handlers(bot):
|
||||
@bot.message_handler(func=lambda msg: msg.text == "Настройки")
|
||||
|
||||
def handle_settings_menu(message: Message):
|
||||
bot.send_message(
|
||||
message.chat.id,
|
||||
"Меню настроек:",
|
||||
reply_markup=get_settings_menu()
|
||||
)
|
||||
@ -1,21 +0,0 @@
|
||||
from telebot.types import Message
|
||||
from app.bot.config import SUPPORT_EMAIL
|
||||
|
||||
|
||||
def register_handlers(bot):
|
||||
@bot.message_handler(func=lambda msg: msg.text == "Регистрация")
|
||||
def handle_registration(message: Message):
|
||||
chat_id = message.chat.id
|
||||
username = message.from_user.username
|
||||
if username:
|
||||
username = f"@{username}"
|
||||
else:
|
||||
username = "N/A"
|
||||
text = (
|
||||
f'Для продолжения регистрации необходимо отправить с корпоративного почтового адреса "РТ МИС" письмо на адрес {SUPPORT_EMAIL}\n'
|
||||
f'В теме письма указать "<b>Подтверждение регистрации в телеграм-боте TeleZab</b>".\n'
|
||||
f'В теле письма указать:\n'
|
||||
f'1. <b>ФИО</b>\n'
|
||||
f'2. <b>Ваш Chat ID</b>: {chat_id}\n'
|
||||
f'3. <b>Ваше имя пользователя</b>: {username}')
|
||||
bot.send_message(chat_id, text, parse_mode="HTML")
|
||||
@ -1,25 +0,0 @@
|
||||
# app/bot/handlers/settings.py
|
||||
from telebot.types import Message
|
||||
from app.bot.keyboards.main_menu import get_main_menu
|
||||
from app.bot.keyboards.settings_menu import get_settings_menu
|
||||
|
||||
def register_handlers(bot):
|
||||
@bot.message_handler(func=lambda msg: msg.text == "Подписаться")
|
||||
def handle_subscribe(message: Message):
|
||||
bot.send_message(message.chat.id, "🔔 Функция подписки ещё не реализована.")
|
||||
|
||||
@bot.message_handler(func=lambda msg: msg.text == "Отписаться")
|
||||
def handle_unsubscribe(message: Message):
|
||||
bot.send_message(message.chat.id, "🔕 Функция отписки ещё не реализована.")
|
||||
|
||||
@bot.message_handler(func=lambda msg: msg.text == "Мои подписки")
|
||||
def handle_my_subscriptions(message: Message):
|
||||
bot.send_message(message.chat.id, "📄 Отображение подписок пока не реализовано.")
|
||||
|
||||
@bot.message_handler(func=lambda msg: msg.text == "Режим уведомлений")
|
||||
def handle_notify_mode(message: Message):
|
||||
bot.send_message(message.chat.id, "⚙️ Настройка режима уведомлений пока не реализована.")
|
||||
|
||||
@bot.message_handler(func=lambda msg: msg.text == "Назад")
|
||||
def handle_back(message: Message):
|
||||
bot.send_message(message.chat.id, "Возврат в главное меню", reply_markup=get_main_menu())
|
||||
@ -1,41 +0,0 @@
|
||||
# app/bot/handlers/start.py
|
||||
from telebot.types import Message, ReplyKeyboardMarkup, KeyboardButton
|
||||
from app.bot.keyboards.main_menu import get_main_menu
|
||||
|
||||
def register_handlers(bot):
|
||||
@bot.message_handler(commands=['start'])
|
||||
def start_handler(message, data=None):
|
||||
chat_id = message.chat.id
|
||||
|
||||
if data:
|
||||
if data.get('user_verified'):
|
||||
user = data['user']
|
||||
bot.send_message(
|
||||
chat_id,
|
||||
f"👋 Привет, {user.user_email}!\nВыберите действие из меню:",
|
||||
reply_markup=get_main_menu()
|
||||
)
|
||||
return
|
||||
|
||||
elif data.get('user_blocked'):
|
||||
bot.send_message(
|
||||
chat_id,
|
||||
"🚫 Ваш аккаунт заблокирован.\n"
|
||||
"Пожалуйста, обратитесь к администратору."
|
||||
)
|
||||
return
|
||||
|
||||
elif data.get('user_not_found'):
|
||||
keyboard = ReplyKeyboardMarkup(resize_keyboard=True)
|
||||
keyboard.add(KeyboardButton("Регистрация"))
|
||||
bot.send_message(
|
||||
chat_id,
|
||||
"👋 Добро пожаловать!\n\n"
|
||||
"❗ Вы не зарегистрированы в системе.\n"
|
||||
"Пожалуйста, нажмите кнопку ниже для регистрации.",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
return
|
||||
|
||||
# fallback
|
||||
bot.send_message(chat_id, "Произошла ошибка. Попробуйте позже.")
|
||||
@ -1,11 +0,0 @@
|
||||
# app/bot/keyboards/main_menu.py
|
||||
from telebot.types import ReplyKeyboardMarkup, KeyboardButton
|
||||
|
||||
def get_main_menu():
|
||||
markup = ReplyKeyboardMarkup(resize_keyboard=True)
|
||||
markup.add(
|
||||
KeyboardButton("Настройки"),
|
||||
KeyboardButton("Активные проблемы"),
|
||||
KeyboardButton("Помощь")
|
||||
)
|
||||
return markup
|
||||
@ -1,9 +0,0 @@
|
||||
# app/bot/keyboards/settings_menu.py
|
||||
from telebot.types import ReplyKeyboardMarkup, KeyboardButton
|
||||
|
||||
def get_settings_menu():
|
||||
markup = ReplyKeyboardMarkup(resize_keyboard=True)
|
||||
markup.add(KeyboardButton("Подписаться"),KeyboardButton("Отписаться"))
|
||||
markup.add(KeyboardButton("Мои подписки"),KeyboardButton("Режим уведомлений"))
|
||||
markup.add(KeyboardButton("Назад"))
|
||||
return markup
|
||||
@ -1,49 +0,0 @@
|
||||
# app/bot/middlewares/user_middleware.py
|
||||
from telebot.handler_backends import BaseMiddleware
|
||||
from app.models.users import Users
|
||||
from app.extensions.db import db
|
||||
|
||||
|
||||
class UserVerificationMiddleware(BaseMiddleware):
|
||||
"""
|
||||
Middleware: проверяет наличие пользователя и флаги, работает в контексте Flask-приложения
|
||||
"""
|
||||
|
||||
def __init__(self, bot, flask_app):
|
||||
super().__init__()
|
||||
self.update_types = ['message', 'callback_query']
|
||||
self.bot = bot
|
||||
self.app = flask_app # Сохраняем ссылку на Flask app
|
||||
|
||||
def pre_process(self, message, data):
|
||||
if hasattr(message, 'chat'):
|
||||
chat_id = message.chat.id
|
||||
elif hasattr(message, 'message') and hasattr(message.message, 'chat'):
|
||||
chat_id = message.message.chat.id
|
||||
else:
|
||||
return
|
||||
|
||||
try:
|
||||
with self.app.app_context():
|
||||
user = db.session.query(Users).filter_by(chat_id=chat_id).first()
|
||||
|
||||
if user is None:
|
||||
data['user_not_found'] = True
|
||||
return
|
||||
|
||||
if user.is_blocked:
|
||||
data['user_blocked'] = True
|
||||
return
|
||||
|
||||
data['user'] = user
|
||||
data['user_verified'] = True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка при проверке пользователя: {e}")
|
||||
|
||||
def post_process(self, message, data, exception=None):
|
||||
if exception:
|
||||
print(f"Exception in handler: {exception}")
|
||||
elif data.get('user_verified'):
|
||||
user = data.get('user')
|
||||
print(f"✅ Пользователь chat_id={user.chat_id} прошёл проверку")
|
||||
@ -1,24 +0,0 @@
|
||||
# app/bot/telezab_bot.py
|
||||
import telebot
|
||||
from app.bot.config import BOT_TOKEN
|
||||
from app.bot.handlers import start, main_menu, settings, help, registration
|
||||
from app.bot.middlewares.user_access import UserVerificationMiddleware
|
||||
from app import create_app
|
||||
|
||||
|
||||
bot = telebot.TeleBot(BOT_TOKEN, use_class_middlewares=True, parse_mode='HTML')
|
||||
flask_app = create_app()
|
||||
|
||||
# Регистрируем обработчики
|
||||
start.register_handlers(bot)
|
||||
main_menu.register_handlers(bot)
|
||||
settings.register_handlers(bot)
|
||||
help.register_handlers(bot)
|
||||
registration.register_handlers(bot)
|
||||
|
||||
# Потом подключаем middleware
|
||||
user_verification_middleware = UserVerificationMiddleware(bot, flask_app)
|
||||
bot.setup_middleware(user_verification_middleware)
|
||||
|
||||
def run_bot():
|
||||
bot.infinity_polling()
|
||||
@ -1,190 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
|
||||
from app.models import AuditLog
|
||||
|
||||
|
||||
class AuditLogger:
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
|
||||
def _save_log(self, ldap_user_id, username, action, details, ipaddress=None):
|
||||
username = username or "Anonymous"
|
||||
ldap_user_id = ldap_user_id or "anonymous"
|
||||
ipaddress = ipaddress or request.headers.get('X-Forwarded-For', request.remote_addr)
|
||||
|
||||
log_entry = AuditLog(
|
||||
ldap_user_id=ldap_user_id,
|
||||
username=username,
|
||||
action=action,
|
||||
details=details,
|
||||
ipaddress=ipaddress
|
||||
)
|
||||
self.session.add(log_entry)
|
||||
self.session.commit()
|
||||
|
||||
def auth(self, username_attempted, success: bool, ldap_user_id=None, display_name=None, error=None):
|
||||
action = "Авторизация" if success else "Ошибка авторизации"
|
||||
details = f"{'Успешный вход' if success else 'Неудачная попытка входа'}: {display_name or username_attempted}"
|
||||
if error:
|
||||
details += f". Ошибка: {error}"
|
||||
self._save_log(
|
||||
ldap_user_id=ldap_user_id or username_attempted,
|
||||
username=display_name if success else "Anonymous",
|
||||
action=action,
|
||||
details=details
|
||||
)
|
||||
|
||||
def users(self, action_type: str, actor_display_name: Optional[str], ldap_user_id: str,
|
||||
affected_chat_id: Optional[int] = None, email: Optional[str] = None,
|
||||
telegram_id: Optional[str] = None, error: Optional[str] = None):
|
||||
action_map = {
|
||||
"add": "Добавление пользователя",
|
||||
"delete": "Удаление пользователя",
|
||||
"block": "Блокировка пользователя",
|
||||
"unblock": "Разблокировка пользователя"
|
||||
}
|
||||
if action_type not in action_map:
|
||||
raise ValueError(f"Недопустимое действие логирования: {action_type}")
|
||||
|
||||
details = self._compose_details(
|
||||
telegram_id=telegram_id,
|
||||
affected_chat_id=affected_chat_id,
|
||||
email=email,
|
||||
error=error,
|
||||
fallback="Данные пользователя отсутствуют" if not current_user.is_authenticated else None
|
||||
)
|
||||
|
||||
self._save_log(
|
||||
ldap_user_id=ldap_user_id,
|
||||
username=actor_display_name,
|
||||
action=action_map[action_type],
|
||||
details=details
|
||||
)
|
||||
|
||||
def systems(self, action_type: str, actor_display_name: Optional[str], ldap_user_id: str,
|
||||
system_id: Optional[str] = None, name: Optional[str] = None, error: Optional[str] = None):
|
||||
action_map = {
|
||||
"add": "Добавление системы",
|
||||
"update": "Изменение имени системы",
|
||||
"delete": "Удаление системы"
|
||||
}
|
||||
if action_type not in action_map:
|
||||
raise ValueError(f"Недопустимое действие логирования: {action_type}")
|
||||
|
||||
details = self._compose_details(
|
||||
system_id=system_id,
|
||||
name=name,
|
||||
error=error
|
||||
)
|
||||
|
||||
self._save_log(
|
||||
ldap_user_id=ldap_user_id,
|
||||
username=actor_display_name,
|
||||
action=action_map[action_type],
|
||||
details=details
|
||||
)
|
||||
|
||||
def regions(self,
|
||||
action_type: str,
|
||||
actor_display_name: Optional[str],
|
||||
ldap_user_id: str,
|
||||
region_id: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
new_name: Optional[str] = None,
|
||||
old_name: Optional[str] = None,
|
||||
active: Optional[bool] = None,
|
||||
old_active: Optional[bool] = None,
|
||||
error: Optional[str] = None,
|
||||
):
|
||||
action_map = {
|
||||
"add": "Добавление региона",
|
||||
"rename": "Изменение имени региона",
|
||||
"toggle": "Изменение статуса региона",
|
||||
"delete": "Удаление региона"
|
||||
}
|
||||
|
||||
if action_type not in action_map:
|
||||
raise ValueError(f"Недопустимое действие логирования: {action_type}")
|
||||
|
||||
if action_type == "rename":
|
||||
if old_name is not None and new_name is not None:
|
||||
details = f"Region id: {region_id}; Rename: {old_name} → {new_name}"
|
||||
else:
|
||||
details = self._compose_details(
|
||||
region_id=region_id,
|
||||
new_name=new_name,
|
||||
old_name=old_name,
|
||||
error=error
|
||||
)
|
||||
|
||||
elif action_type == "toggle":
|
||||
if old_active is not None and active is not None:
|
||||
old_status_str = "Активен" if old_active else "Отключён"
|
||||
new_status_str = "Активен" if active else "Отключён"
|
||||
details = f"Region id: {region_id}; Status: {old_status_str} → {new_status_str}"
|
||||
else:
|
||||
status_str = "Активен" if active else "Отключён" if active is not None else None
|
||||
details = self._compose_details(
|
||||
region_id=region_id,
|
||||
status=status_str,
|
||||
error=error
|
||||
)
|
||||
|
||||
else:
|
||||
status_str = "Активен" if active else "Отключён" if active is not None else None
|
||||
details = self._compose_details(
|
||||
region_id=region_id,
|
||||
name=name,
|
||||
status=status_str,
|
||||
error=error
|
||||
)
|
||||
|
||||
self._save_log(
|
||||
ldap_user_id=ldap_user_id,
|
||||
username=actor_display_name,
|
||||
action=action_map[action_type],
|
||||
details=details
|
||||
)
|
||||
|
||||
|
||||
def get_web_action_logs(self, page, per_page, ldap_user_id_filter=None, action_filter=None):
|
||||
query = AuditLog.query.order_by(AuditLog.timestamp.desc())
|
||||
|
||||
if ldap_user_id_filter:
|
||||
query = query.filter_by(ldap_user_id=ldap_user_id_filter)
|
||||
if action_filter:
|
||||
query = query.filter(AuditLog.action.like(f'%{action_filter}%'))
|
||||
|
||||
pagination = query.paginate(page=page, per_page=per_page)
|
||||
return {
|
||||
'logs': [
|
||||
{
|
||||
'id': log.id,
|
||||
'ldap_user_id': log.ldap_user_id,
|
||||
'username': log.username,
|
||||
'timestamp': log.timestamp.isoformat(),
|
||||
'action': log.action,
|
||||
'details': log.details
|
||||
} for log in pagination.items
|
||||
],
|
||||
'total': pagination.total,
|
||||
'pages': pagination.pages,
|
||||
'current_page': pagination.page,
|
||||
'per_page': pagination.per_page
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _compose_details(**kwargs) -> str:
|
||||
parts = []
|
||||
for key, value in kwargs.items():
|
||||
if value is None:
|
||||
continue
|
||||
if key == "fallback":
|
||||
parts.append(value)
|
||||
else:
|
||||
key_display = key.replace("_", " ").capitalize()
|
||||
parts.append(f"{key_display}: {value}")
|
||||
return "; ".join(parts)
|
||||
@ -1,10 +0,0 @@
|
||||
# backend/ext/auth_ext.py (опционально, если хочешь инициализировать через factory)
|
||||
from flask_login import LoginManager
|
||||
from app.services.auth_service import init_ldap
|
||||
|
||||
login_manager = LoginManager()
|
||||
|
||||
def init_auth(app):
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = 'auth.login'
|
||||
init_ldap(app)
|
||||
@ -1,3 +0,0 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db: SQLAlchemy = SQLAlchemy()
|
||||
@ -1,8 +0,0 @@
|
||||
from .users import Users
|
||||
from .regions import Regions
|
||||
from .subscriptions import Subscriptions
|
||||
from .userevents import UserEvents
|
||||
from .auditlog import AuditLog
|
||||
from .systems import Systems
|
||||
from .user import User
|
||||
from .state import UserState
|
||||
@ -1,19 +0,0 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Integer, String, DateTime
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.extensions.db import db
|
||||
|
||||
class AuditLog(db.Model):
|
||||
__tablename__ = 'auditlog'
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
ldap_user_id: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
username: Mapped[str | None] = mapped_column(String(255))
|
||||
action: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
details: Mapped[str | None] = mapped_column(String(1024))
|
||||
timestamp: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
ipaddress: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
def __repr__(self):
|
||||
return f"<AuditLog(ldap_user_id='{self.ldap_user_id}', username='{self.username}', action='{self.action}', timestamp='{self.timestamp}')>"
|
||||
@ -1,6 +0,0 @@
|
||||
from app.extensions.db import db
|
||||
|
||||
class Regions(db.Model):
|
||||
region_id = db.Column(db.Integer, primary_key=True)
|
||||
region_name = db.Column(db.String(255), nullable=False)
|
||||
active = db.Column(db.Boolean, default=True)
|
||||
@ -1,10 +0,0 @@
|
||||
from app.extensions.db import db
|
||||
|
||||
class UserState(db.Model):
|
||||
__tablename__ = "user_states"
|
||||
|
||||
chat_id = db.Column(db.BigInteger, primary_key=True)
|
||||
state = db.Column(db.String(64), nullable=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<UserState chat_id={self.chat_id}, state={self.state}>"
|
||||
@ -1,13 +0,0 @@
|
||||
from sqlalchemy import ForeignKey, PrimaryKeyConstraint
|
||||
|
||||
from app.extensions.db import db
|
||||
|
||||
class Subscriptions(db.Model):
|
||||
chat_id = db.Column(db.Integer, ForeignKey('users.chat_id', ondelete='CASCADE'), nullable=False) #Добавляем внешний ключ с ondelete
|
||||
region_id = db.Column(db.Integer, nullable=False)
|
||||
active = db.Column(db.Boolean, default=True)
|
||||
skip = db.Column(db.Boolean, default=False)
|
||||
disaster_only = db.Column(db.Boolean, default=False)
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint('chat_id', 'region_id'),
|
||||
)
|
||||
@ -1,10 +0,0 @@
|
||||
from app.extensions.db import db
|
||||
|
||||
class Systems(db.Model):
|
||||
__tablename__ = 'systems'
|
||||
system_id = db.Column(db.Integer, primary_key=True)
|
||||
system_name = db.Column(db.String(255), nullable=False)
|
||||
name = db.Column(db.String(255), nullable=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<System {self.system_id}: {self.system_name} {self.name}>'
|
||||
@ -1,17 +0,0 @@
|
||||
from flask_login import UserMixin
|
||||
|
||||
|
||||
class User(UserMixin):
|
||||
def __init__(self, user_id,
|
||||
user_name=None,
|
||||
user_surname=None,
|
||||
user_middle_name=None,
|
||||
display_name=None,
|
||||
email=None
|
||||
):
|
||||
self.id = str(user_id)
|
||||
self.user_name = user_name
|
||||
self.user_surname = user_surname
|
||||
self.user_middle_name = user_middle_name
|
||||
self.display_name = display_name
|
||||
self.email = email
|
||||
@ -1,9 +0,0 @@
|
||||
from app.extensions.db import db
|
||||
|
||||
|
||||
class UserEvents(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
chat_id = db.Column(db.Integer, nullable=False)
|
||||
telegram_id = 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())
|
||||
@ -1,10 +0,0 @@
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.extensions.db import db
|
||||
|
||||
class Users(db.Model):
|
||||
chat_id = db.Column(db.Integer, primary_key=True)
|
||||
telegram_id = db.Column(db.String(80), unique=True, nullable=False)
|
||||
user_email = db.Column(db.String(255), unique=True, nullable=False)
|
||||
is_blocked = db.Column(db.Boolean, default=False)
|
||||
subscriptions = relationship("Subscriptions", backref="user", cascade="all, delete-orphan") # Добавлено cascade
|
||||
@ -1,8 +0,0 @@
|
||||
from .auth import auth_bp
|
||||
from .dashboard import dashboard_bp
|
||||
from .api import api_bp
|
||||
|
||||
def register_blueprints(app):
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(dashboard_bp)
|
||||
app.register_blueprint(api_bp)
|
||||
@ -1,14 +0,0 @@
|
||||
from flask import Blueprint
|
||||
|
||||
api_bp = Blueprint('api', __name__, url_prefix='/telezab/rest/api')
|
||||
|
||||
from .regions import region_bp
|
||||
from .users import users_bp
|
||||
from .systems import system_bp
|
||||
from .notifications import notification_bp
|
||||
|
||||
# Регистрируем вложенные блюпринты с url_prefix
|
||||
api_bp.register_blueprint(region_bp, url_prefix='/regions')
|
||||
api_bp.register_blueprint(users_bp, url_prefix='/users')
|
||||
api_bp.register_blueprint(system_bp, url_prefix='/systems')
|
||||
api_bp.register_blueprint(notification_bp, url_prefix='/notifications')
|
||||
@ -1,12 +0,0 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app.services.notifications_service import NotificationService
|
||||
|
||||
notification_bp = Blueprint('notification', __name__,url_prefix='/notifications')
|
||||
|
||||
|
||||
@notification_bp.route('/', methods=['POST'], strict_slashes=False)
|
||||
def notification():
|
||||
service = NotificationService()
|
||||
data = request.get_json()
|
||||
result, status = service.process_notification(data)
|
||||
return jsonify(result), status
|
||||
@ -1,42 +0,0 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
from app.services.regions_service import RegionService
|
||||
|
||||
region_bp = Blueprint('region', __name__,url_prefix='/regions')
|
||||
region = RegionService()
|
||||
|
||||
@region_bp.route('/', methods=['GET'], strict_slashes=False)
|
||||
@login_required
|
||||
def list_regions():
|
||||
return jsonify(region.get_regions(
|
||||
page=request.args.get('page', 1, type=int),
|
||||
per_page=request.args.get('per_page', 10, type=int),
|
||||
sort_field=request.args.get('sort_field', 'region_id'),
|
||||
sort_order=request.args.get('sort_order', 'asc')
|
||||
))
|
||||
|
||||
@region_bp.route('/', methods=['POST'], strict_slashes=False)
|
||||
@login_required
|
||||
def add_region():
|
||||
return region.add_region(request.json, current_user)
|
||||
|
||||
@region_bp.route('/<int:region_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_region(region_id):
|
||||
return region.delete_region(region_id, current_user)
|
||||
|
||||
@region_bp.route('/name', methods=['PUT'])
|
||||
@login_required
|
||||
def update_name():
|
||||
return region.update_region_name(request.json, current_user)
|
||||
|
||||
@region_bp.route('/status', methods=['PUT'])
|
||||
@login_required
|
||||
def update_status():
|
||||
return region.update_region_status(request.json, current_user)
|
||||
|
||||
@region_bp.route('/<int:region_id>/subscribers', methods=['GET'])
|
||||
@login_required
|
||||
def region_subscribers(region_id):
|
||||
return region.get_region_subscribers(region_id)
|
||||
@ -1,31 +0,0 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask_login import login_required
|
||||
from app.services.systems_service import SystemService
|
||||
|
||||
system_bp = Blueprint('system', __name__,url_prefix='/systems')
|
||||
system = SystemService()
|
||||
|
||||
@system_bp.route('/', methods=['GET'], strict_slashes=False)
|
||||
@login_required
|
||||
def list_systems():
|
||||
return jsonify(system.get_systems(
|
||||
page=request.args.get('page', 1, type=int),
|
||||
per_page=request.args.get('per_page', 10, type=int),
|
||||
sort_field=request.args.get('sort_field', 'system_id'),
|
||||
sort_order=request.args.get('sort_order', 'asc')
|
||||
))
|
||||
|
||||
@system_bp.route('/', methods=['POST'],strict_slashes=False)
|
||||
@login_required
|
||||
def add_system():
|
||||
return system.add_system(request.json)
|
||||
|
||||
@system_bp.route('/', methods=['PUT'],strict_slashes=False)
|
||||
@login_required
|
||||
def update_system():
|
||||
return system.update_system_name(request.json)
|
||||
|
||||
@system_bp.route('/<int:system_id>', methods=['DELETE'],strict_slashes=False)
|
||||
@login_required
|
||||
def delete_system(system_id):
|
||||
return system.delete_system(system_id)
|
||||
@ -1,68 +0,0 @@
|
||||
# app/routes/users.py
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
# Импортируем функции сервисов напрямую
|
||||
from app.services.users_service import get_users, get_user, toggle_block_user, delete_user, add_user, search_users
|
||||
from app.services.users_event_service import log_user_action, get_user_events # Импортируем функции
|
||||
|
||||
users_bp = Blueprint('users', __name__, url_prefix='/users')
|
||||
|
||||
@users_bp.route('/', methods=['GET', 'POST'], strict_slashes=False)
|
||||
@login_required
|
||||
def manage_users_route():
|
||||
if request.method == 'GET':
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
return jsonify(get_users(page, per_page))
|
||||
elif request.method == 'POST':
|
||||
user_data = request.get_json()
|
||||
result, status_code = add_user(user_data, current_user)
|
||||
return jsonify(result), status_code
|
||||
return None
|
||||
|
||||
|
||||
@users_bp.route('/<int:chat_id>', methods=['GET'])
|
||||
@login_required
|
||||
def get_user_route(chat_id):
|
||||
user = get_user(chat_id)
|
||||
if not user:
|
||||
return jsonify({'error': 'Пользователь не найден'}), 404
|
||||
return jsonify(user)
|
||||
|
||||
@users_bp.route('/<int:chat_id>/block', methods=['POST'])
|
||||
@login_required
|
||||
def block_user_route(chat_id):
|
||||
result, status_code = toggle_block_user(chat_id, current_user)
|
||||
return jsonify(result), status_code
|
||||
|
||||
@users_bp.route('/<int:chat_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_user_route(chat_id):
|
||||
result, status_code = delete_user(chat_id, current_user)
|
||||
return jsonify(result), status_code
|
||||
|
||||
@users_bp.route('/<int:chat_id>/log', methods=['POST'])
|
||||
@login_required
|
||||
def log_user_action_route(chat_id):
|
||||
action = request.json.get('action')
|
||||
if action:
|
||||
result, status_code = log_user_action(chat_id, action) # Вызываем функцию напрямую
|
||||
return jsonify(result), status_code
|
||||
else:
|
||||
return jsonify({'error': 'Не указано действие'}), 400
|
||||
|
||||
@users_bp.route('/search', methods=['GET'])
|
||||
@login_required
|
||||
def search_users_route():
|
||||
telegram_id = request.args.get('telegram_id')
|
||||
email = request.args.get('email')
|
||||
users = search_users(telegram_id, email)
|
||||
return jsonify(users)
|
||||
|
||||
@users_bp.route('/<int:chat_id>/user_events', methods=['GET'])
|
||||
@login_required
|
||||
def handle_user_events_route(chat_id):
|
||||
result, status_code = get_user_events(chat_id) # Вызываем функцию напрямую
|
||||
return jsonify(result), status_code
|
||||
@ -1,59 +0,0 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, session, current_app
|
||||
from flask_login import login_user, login_required, logout_user
|
||||
|
||||
from app.extensions.db import db
|
||||
from app.extensions.audit_logger import AuditLogger
|
||||
from app.services.auth_service import authenticate_user, parse_ldap_user
|
||||
from app.models import User
|
||||
|
||||
|
||||
auditlog = AuditLogger(db.session)
|
||||
|
||||
auth_bp = Blueprint('auth', __name__, url_prefix='/telezab/')
|
||||
|
||||
@auth_bp.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if 'user_id' in session:
|
||||
return redirect(url_for('dashboard.dashboard'))
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
|
||||
success, user_info, error = authenticate_user(username, password)
|
||||
|
||||
if not success:
|
||||
flash(error, 'danger')
|
||||
auditlog.auth(username_attempted=username, success=False, error=error)
|
||||
return render_template("login.html")
|
||||
|
||||
data = parse_ldap_user(user_info)
|
||||
display_name = (f"{data['user_surname']} {data['user_name']} {data['user_middle_name']}").strip()
|
||||
user = User(
|
||||
user_id=data['sam_account_name'],
|
||||
user_name=data['user_name'],
|
||||
user_surname=data['user_surname'],
|
||||
user_middle_name=data['user_middle_name'],
|
||||
display_name=display_name,
|
||||
email=data['email']
|
||||
)
|
||||
|
||||
session.permanent = True
|
||||
session['username'] = data['sam_account_name']
|
||||
session['display_name'] = display_name
|
||||
session['user_data'] = data
|
||||
login_user(user)
|
||||
|
||||
auditlog.auth(username_attempted=username, success=True, ldap_user_id=data['sam_account_name'], display_name=display_name)
|
||||
flash("Logged in successfully!", "success")
|
||||
return redirect(url_for("dashboard.dashboard"))
|
||||
|
||||
return render_template("login.html")
|
||||
|
||||
|
||||
@auth_bp.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
session.clear()
|
||||
return redirect(url_for('auth.login'))
|
||||
@ -1,92 +0,0 @@
|
||||
from flask import Blueprint, render_template, request
|
||||
|
||||
from app.models import AuditLog
|
||||
from app.models import Users
|
||||
from flask_login import login_required
|
||||
|
||||
# Создаём Blueprint
|
||||
dashboard_bp = Blueprint('dashboard', __name__, url_prefix='/telezab/')
|
||||
|
||||
|
||||
|
||||
# Роуты для отображения страниц
|
||||
@dashboard_bp.route('/')
|
||||
@login_required
|
||||
def dashboard():
|
||||
return render_template('index.html')
|
||||
|
||||
@dashboard_bp.route('/users')
|
||||
@login_required
|
||||
def users_page():
|
||||
users = Users.query.all()
|
||||
return render_template('users.html', user=users)
|
||||
|
||||
@dashboard_bp.route('/logs')
|
||||
@login_required
|
||||
def logs_page():
|
||||
# Получаем параметры фильтрации из query string
|
||||
action = request.args.get('action', type=str)
|
||||
username = request.args.get('username', type=str)
|
||||
timestamp_from = request.args.get('timestamp_from', type=str)
|
||||
timestamp_to = request.args.get('timestamp_to', type=str)
|
||||
order = request.args.get('order', 'asc').lower()
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = 20
|
||||
|
||||
query = AuditLog.query
|
||||
|
||||
# Фильтрация по action
|
||||
if action:
|
||||
query = query.filter(AuditLog.action.ilike(f'%{action}%'))
|
||||
|
||||
# Фильтрация по username
|
||||
if username:
|
||||
query = query.filter(AuditLog.username.ilike(f'%{username}%'))
|
||||
|
||||
# Фильтрация по дате (начало)
|
||||
if timestamp_from:
|
||||
try:
|
||||
from datetime import datetime
|
||||
dt_from = datetime.strptime(timestamp_from, '%Y-%m-%d')
|
||||
query = query.filter(AuditLog.timestamp >= dt_from)
|
||||
except ValueError:
|
||||
pass # Игнорируем неверный формат
|
||||
|
||||
# Фильтрация по дате (конец)
|
||||
if timestamp_to:
|
||||
try:
|
||||
from datetime import datetime, timedelta
|
||||
dt_to = datetime.strptime(timestamp_to, '%Y-%m-%d') + timedelta(days=1)
|
||||
query = query.filter(AuditLog.timestamp < dt_to)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Сортировка
|
||||
if order == 'asc':
|
||||
query = query.order_by(AuditLog.timestamp.asc())
|
||||
else:
|
||||
query = query.order_by(AuditLog.timestamp.desc())
|
||||
|
||||
# Пагинация
|
||||
logs = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
|
||||
# Передаем текущие фильтры и сортировку в шаблон для отображения и генерации ссылок
|
||||
filters = {
|
||||
'action': action,
|
||||
'username': username,
|
||||
'timestamp_from': timestamp_from,
|
||||
'timestamp_to': timestamp_to,
|
||||
'order': order
|
||||
}
|
||||
|
||||
return render_template('logs.html', logs=logs, filters=filters)
|
||||
|
||||
@dashboard_bp.route('/regions')
|
||||
@login_required
|
||||
def regions_page():
|
||||
return render_template('regions.html')
|
||||
|
||||
@dashboard_bp.route('/health')
|
||||
def healthcheck():
|
||||
pass
|
||||
|
||||
@ -1,53 +0,0 @@
|
||||
|
||||
from flask import current_app
|
||||
from flask_ldap3_login import LDAP3LoginManager, AuthenticationResponseStatus
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
import config
|
||||
|
||||
def init_ldap(app):
|
||||
app.config['LDAP_HOST'] = config.LDAP_HOST
|
||||
app.config['LDAP_PORT'] = config.LDAP_PORT
|
||||
app.config['LDAP_USE_SSL'] = config.LDAP_USE_SSL
|
||||
app.config['LDAP_BASE_DN'] = config.LDAP_BASE_DN
|
||||
app.config['LDAP_BIND_DIRECT_CREDENTIALS'] = False
|
||||
app.config['LDAP_BIND_USER_DN'] = config.LDAP_BIND_USER_DN
|
||||
app.config['LDAP_BIND_USER_PASSWORD'] = config.LDAP_USER_PASSWORD
|
||||
app.config['LDAP_USER_DN'] = config.LDAP_USER_DN
|
||||
app.config['LDAP_USER_PASSWORD'] = config.LDAP_USER_PASSWORD
|
||||
app.config['LDAP_USER_OBJECT_FILTER'] = config.LDAP_USER_OBJECT_FILTER
|
||||
app.config['LDAP_USER_LOGIN_ATTR'] = config.LDAP_USER_LOGIN_ATTR
|
||||
app.config['LDAP_USER_SEARCH_SCOPE'] = config.LDAP_USER_SEARCH_SCOPE
|
||||
app.config['LDAP_SCHEMA'] = config.LDAP_SCHEMA
|
||||
|
||||
ldap_manager = LDAP3LoginManager(app)
|
||||
app.extensions['ldap3_login'] = ldap_manager
|
||||
ldap_manager.init_app(app)
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1)
|
||||
|
||||
def authenticate_user(username, password):
|
||||
ldap_manager = current_app.extensions['ldap3_login']
|
||||
response = ldap_manager.authenticate(username, password)
|
||||
if response.status == AuthenticationResponseStatus.success:
|
||||
return True, response.user_info, None
|
||||
elif response.status == AuthenticationResponseStatus.fail:
|
||||
return False, None, "Invalid username or password."
|
||||
else:
|
||||
return False, None, f"LDAP Error: {response.status}"
|
||||
|
||||
def parse_ldap_user(user_info):
|
||||
def get(attr):
|
||||
value = user_info.get(attr)
|
||||
if isinstance(value, list) and value:
|
||||
return str(value[0])
|
||||
elif value:
|
||||
return str(value)
|
||||
else:
|
||||
return None
|
||||
|
||||
return {
|
||||
'sam_account_name': get("sAMAccountName"),
|
||||
'email': get("mail"),
|
||||
'user_name': get("givenName"),
|
||||
'user_middle_name': get("middleName"),
|
||||
'user_surname': get("sn"),
|
||||
}
|
||||
@ -1,72 +0,0 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from app.extensions.db import db
|
||||
from app.models import Users, Regions, Subscriptions, UserEvents
|
||||
|
||||
import telebot # Для логов, можно заменить на кастомный логгер
|
||||
|
||||
|
||||
def is_whitelisted(chat_id):
|
||||
"""Проверяет, есть ли пользователь с заданным chat_id в базе данных и не заблокирован ли он."""
|
||||
try:
|
||||
user = Users.query.filter_by(chat_id=chat_id).first()
|
||||
if user:
|
||||
if user.is_blocked:
|
||||
return False, "Ваш доступ заблокирован."
|
||||
return True, None
|
||||
return False, None
|
||||
except Exception as e:
|
||||
telebot.logger.error(f"Ошибка при проверке пользователя: {e}")
|
||||
return False, "Произошла ошибка при проверке доступа."
|
||||
|
||||
|
||||
def get_sorted_regions():
|
||||
"""Получить список активных регионов, отсортированных по region_id."""
|
||||
return (
|
||||
Regions.query
|
||||
.filter_by(active=True)
|
||||
.order_by(Regions.region_id.asc())
|
||||
.with_entities(Regions.region_id, Regions.region_name)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def get_user_subscribed_regions(chat_id):
|
||||
"""Получить список регионов, на которые подписан пользователь."""
|
||||
return (
|
||||
Regions.query
|
||||
.join(Subscriptions, Subscriptions.region_id == Regions.region_id)
|
||||
.filter(
|
||||
Subscriptions.chat_id == chat_id,
|
||||
Subscriptions.active.is_(True),
|
||||
Subscriptions.skip.is_(False)
|
||||
)
|
||||
.order_by(Regions.region_id.asc())
|
||||
.with_entities(Regions.region_id, Regions.region_name)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def format_regions_list(regions):
|
||||
"""Сформировать строку для отображения списка регионов."""
|
||||
return '\n'.join([f"{region_id} - {region_name}" for region_id, region_name in regions])
|
||||
|
||||
|
||||
def log_user_event(chat_id, username, action):
|
||||
"""Логирует действие пользователя."""
|
||||
try:
|
||||
timestamp = datetime.now(timezone.utc)
|
||||
event = UserEvents(
|
||||
chat_id=chat_id,
|
||||
telegram_id=username,
|
||||
action=action,
|
||||
timestamp=timestamp
|
||||
)
|
||||
db.session.add(event)
|
||||
db.session.commit()
|
||||
|
||||
formatted_time = timestamp.strftime('%Y-%m-%d %H:%M:%S')
|
||||
telebot.logger.info(f"User event logged: {chat_id} ({username}) - {action} at {formatted_time}.")
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
telebot.logger.error(f"Error logging user event: {e}")
|
||||
@ -1,28 +0,0 @@
|
||||
from utilities.notification_manager import NotificationManager
|
||||
from utilities.telegram_utilities import extract_region_number, format_message
|
||||
from flask import current_app
|
||||
|
||||
|
||||
|
||||
class NotificationService:
|
||||
def __init__(self):
|
||||
self.logger = current_app.logger
|
||||
self.manager = NotificationManager(self.logger)
|
||||
|
||||
def process_notification(self, data):
|
||||
self.logger.info(f"Получены данные уведомления: {data}")
|
||||
|
||||
region_id = extract_region_number(data.get("host"))
|
||||
if region_id is None:
|
||||
self.logger.error(f"Не удалось извлечь номер региона из host: {data.get('host')}")
|
||||
return {"status": "error", "message": "Invalid host format"}, 400
|
||||
|
||||
self.logger.debug(f"Извлечён номер региона: {region_id}")
|
||||
|
||||
subscribers = self.manager.get_subscribers(region_id, data['severity'])
|
||||
|
||||
if self.manager.is_region_active(region_id):
|
||||
message = format_message(data)
|
||||
self.manager.send_notifications(subscribers, message)
|
||||
|
||||
return {"status": "success"}, 200
|
||||
@ -1,86 +0,0 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from app import app, Users
|
||||
import aio_pika
|
||||
import pika
|
||||
|
||||
from config import RABBITMQ_LOGIN, RABBITMQ_PASS, RABBITMQ_HOST, RABBITMQ_QUEUE, RABBITMQ_URL_FULL
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
rate_limit_semaphore = asyncio.Semaphore(25)
|
||||
|
||||
def rabbitmq_connection():
|
||||
credentials = pika.PlainCredentials(RABBITMQ_LOGIN, RABBITMQ_PASS)
|
||||
parameters = pika.ConnectionParameters(
|
||||
host=RABBITMQ_HOST,
|
||||
credentials=credentials,
|
||||
heartbeat=600,
|
||||
blocked_connection_timeout=300
|
||||
)
|
||||
connection = pika.BlockingConnection(parameters)
|
||||
channel = connection.channel()
|
||||
channel.queue_declare(queue=RABBITMQ_QUEUE, durable=True)
|
||||
return connection, channel
|
||||
|
||||
def send_to_queue(message):
|
||||
connection, channel = rabbitmq_connection()
|
||||
channel.basic_publish(
|
||||
exchange='',
|
||||
routing_key=RABBITMQ_QUEUE,
|
||||
body=json.dumps(message),
|
||||
properties=pika.BasicProperties(
|
||||
delivery_mode=2,
|
||||
))
|
||||
connection.close()
|
||||
|
||||
async def send_message(chat_id, message, backend_bot, is_notification=False):
|
||||
telegram_id = "unknown"
|
||||
try:
|
||||
if is_notification:
|
||||
await rate_limit_semaphore.acquire()
|
||||
|
||||
def get_user():
|
||||
with app.app_context():
|
||||
user = Users.query.get(chat_id)
|
||||
return user.telegram_id if user else "unknown"
|
||||
|
||||
telegram_id = await asyncio.to_thread(get_user)
|
||||
|
||||
await asyncio.to_thread(backend_bot.bot.send_message, chat_id, message, parse_mode='HTML')
|
||||
|
||||
formatted_message = message.replace('\n', ' ').replace('\r', '')
|
||||
logger.info(f'Send notification to {telegram_id} ({chat_id}) from RabbitMQ [{formatted_message}]')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending message to {telegram_id} ({chat_id}): {e}")
|
||||
finally:
|
||||
if is_notification:
|
||||
rate_limit_semaphore.release()
|
||||
|
||||
async def consume_from_queue(backend_bot):
|
||||
while True:
|
||||
try:
|
||||
connection = await aio_pika.connect_robust(RABBITMQ_URL_FULL)
|
||||
async with connection:
|
||||
channel = await connection.channel()
|
||||
queue = await channel.declare_queue(RABBITMQ_QUEUE, durable=True)
|
||||
|
||||
async for message in queue:
|
||||
async with message.process():
|
||||
try:
|
||||
data = json.loads(message.body.decode('utf-8'))
|
||||
chat_id = data["chat_id"]
|
||||
message_text = data["message"]
|
||||
await send_message(chat_id, message_text, backend_bot, is_notification=True)
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
logger.error(f"Error processing message: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending message: {e}")
|
||||
except aio_pika.exceptions.AMQPError as e:
|
||||
logger.error(f"RabbitMQ error: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Critical error: {e}")
|
||||
finally:
|
||||
await asyncio.sleep(5)
|
||||
@ -1,299 +0,0 @@
|
||||
from flask import current_app
|
||||
from sqlalchemy import desc, asc
|
||||
import logging
|
||||
from app import Regions, db, Users, Subscriptions
|
||||
from app.extensions.audit_logger import AuditLogger
|
||||
|
||||
|
||||
auditlog = AuditLogger(db.session)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
class RegionService:
|
||||
def __init__(self):
|
||||
self.db = db
|
||||
self.auditlog = auditlog
|
||||
self.logger = logger
|
||||
|
||||
def get_regions(self, page=1, per_page=10, sort_field='region_id', sort_order='asc'):
|
||||
self.logger.info(f"Получение регионов: page={page}, per_page={per_page}, sort_field={sort_field}, sort_order={sort_order}")
|
||||
|
||||
# Определение порядка сортировки
|
||||
sort_func = asc if sort_order == 'asc' else desc
|
||||
|
||||
# Получение атрибута модели для сортировки
|
||||
if sort_field:
|
||||
sort_attr = getattr(Regions, sort_field, Regions.region_id) # По умолчанию сортируем по region_id
|
||||
else:
|
||||
sort_attr = Regions.region_id
|
||||
|
||||
# Запрос к базе данных с учетом сортировки и пагинации
|
||||
if sort_field == 'region_id':
|
||||
regions_query = Regions.query.order_by(sort_func(Regions.region_id.cast(db.Integer))).paginate(page=page, per_page=per_page, error_out=False)
|
||||
elif sort_field == 'name':
|
||||
regions_query = Regions.query.order_by(sort_func(Regions.region_name)).paginate(page=page, per_page=per_page, error_out=False)
|
||||
else:
|
||||
regions_query = Regions.query.order_by(sort_func(sort_attr)).paginate(page=page, per_page=per_page, error_out=False)
|
||||
|
||||
regions_list = [{
|
||||
'region_id': r.region_id,
|
||||
'name': r.region_name,
|
||||
'active': r.active
|
||||
} for r in regions_query.items]
|
||||
|
||||
# bf.app.logger.info(f"Получены регионы: {len(regions_list)} элементов")
|
||||
|
||||
return {
|
||||
'regions': regions_list,
|
||||
'total_regions': regions_query.total,
|
||||
'total_pages': regions_query.pages,
|
||||
'current_page': regions_query.page,
|
||||
'per_page': regions_query.per_page
|
||||
}
|
||||
|
||||
def get_region_by_id(self, region_id):
|
||||
# bf.app.logger.info(f"Поиск региона по ID: {region_id}")
|
||||
|
||||
# Получение региона по его ID
|
||||
region = Regions.query.filter_by(region_id=region_id).first()
|
||||
|
||||
if region:
|
||||
# bf.app.logger.info(f"Найден регион: {region.region_name}")
|
||||
return region.region_name
|
||||
else:
|
||||
# bf.app.logger.warning(f"Регион с ID {region_id} не найден.")
|
||||
return None
|
||||
|
||||
def get_region_subscribers(self, region_id):
|
||||
# bf.app.logger.info(f"Получение подписчиков региона: region_id={region_id}")
|
||||
|
||||
try:
|
||||
region = Regions.query.get(region_id)
|
||||
if not region:
|
||||
# bf.app.logger.warning(f"Регион с ID {region_id} не найден")
|
||||
return {'status': 'error', 'message': 'Регион не найден'}, 404
|
||||
|
||||
subscribers = self.db.session.query(
|
||||
Users).join(Subscriptions).filter(Subscriptions.region_id == region_id).all()
|
||||
|
||||
subscribers_list = [{
|
||||
'chat_id': user.chat_id,
|
||||
'telegram_id': user.telegram_id,
|
||||
'email': user.user_email
|
||||
} for user in subscribers]
|
||||
|
||||
# bf.app.logger.info(f"Получены подписчики региона {region_id}: {len(subscribers_list)} элементов")
|
||||
|
||||
return {'status': 'success', 'subscribers': subscribers_list}, 200
|
||||
except Exception as e:
|
||||
# bf.app.logger.error(f"Ошибка при получении подписчиков региона: {e}")
|
||||
return {'status': 'error', 'message': str(e)}, 500
|
||||
|
||||
def add_region(self, data, user):
|
||||
region_id = data.get('region_id')
|
||||
name = data.get('name')
|
||||
active = data.get('active', True)
|
||||
|
||||
self.logger.info(f"Добавление региона: region_id={region_id}, name={name}, active={active}")
|
||||
|
||||
try:
|
||||
if not region_id.isdigit():
|
||||
self.logger.warning(f"ID региона {region_id} содержит нечисловые символы")
|
||||
error_msg = 'ID региона должен содержать только числа.'
|
||||
self.auditlog.regions(
|
||||
action_type="add",
|
||||
actor_display_name=user.display_name,
|
||||
ldap_user_id=user.id,
|
||||
name=name,
|
||||
region_id=region_id,
|
||||
error=error_msg
|
||||
)
|
||||
return {'status': 'error', 'message': error_msg}, 400
|
||||
|
||||
existing_region = Regions.query.get(region_id)
|
||||
if existing_region:
|
||||
self.logger.warning(f"Регион с ID {region_id} уже существует")
|
||||
error_msg = 'Регион с таким ID уже существует'
|
||||
self.auditlog.regions(
|
||||
action_type="add",
|
||||
actor_display_name=user.display_name,
|
||||
ldap_user_id=user.id,
|
||||
name=name,
|
||||
region_id=region_id,
|
||||
error=error_msg
|
||||
)
|
||||
return {'status': 'error', 'message': error_msg}, 409
|
||||
|
||||
region = Regions(region_id=region_id, region_name=name, active=active)
|
||||
self.db.session.add(region)
|
||||
self.db.session.commit()
|
||||
|
||||
self.logger.info(f"Регион {region_id} успешно добавлен")
|
||||
self.auditlog.regions(
|
||||
action_type="add",
|
||||
actor_display_name=user.display_name,
|
||||
ldap_user_id=user.id,
|
||||
name=name,
|
||||
region_id=region_id
|
||||
)
|
||||
return {'status': 'success', 'message': 'Регион добавлен'}, 201
|
||||
|
||||
except Exception as e:
|
||||
self.db.session.rollback()
|
||||
error_msg = str(e)
|
||||
self.logger.error(f"Ошибка при добавлении региона: {error_msg}")
|
||||
self.auditlog.regions(
|
||||
action_type="add",
|
||||
actor_display_name=user.display_name,
|
||||
ldap_user_id=user.id,
|
||||
name=name,
|
||||
region_id=region_id,
|
||||
error=error_msg
|
||||
)
|
||||
return {'status': 'error', 'message': error_msg}, 500
|
||||
|
||||
def update_region_status(self, data, user):
|
||||
region_id = data.get('region_id')
|
||||
new_status = data.get('active')
|
||||
|
||||
self.logger.info(f"Изменение статуса региона: region_id={region_id}, new_status={new_status}")
|
||||
|
||||
try:
|
||||
region = Regions.query.get(region_id)
|
||||
if region:
|
||||
old_status = region.active
|
||||
region.active = new_status
|
||||
self.db.session.commit()
|
||||
|
||||
self.logger.info(f"Статус региона {region_id} изменён: {old_status} → {new_status}")
|
||||
self.auditlog.regions(
|
||||
action_type="toggle",
|
||||
actor_display_name=user.display_name,
|
||||
ldap_user_id=user.id,
|
||||
region_id=region_id,
|
||||
old_active=old_status,
|
||||
active=new_status
|
||||
)
|
||||
|
||||
return {'status': 'success', 'message': 'Статус региона обновлён'}, 200
|
||||
else:
|
||||
error_msg = f"Регион с ID {region_id} не найден"
|
||||
self.logger.warning(error_msg)
|
||||
self.auditlog.regions(
|
||||
action_type="toggle",
|
||||
actor_display_name=user.display_name,
|
||||
ldap_user_id=user.id,
|
||||
region_id=region_id,
|
||||
active=new_status,
|
||||
error=error_msg
|
||||
)
|
||||
return {'status': 'error', 'message': 'Регион не найден'}, 404
|
||||
except Exception as e:
|
||||
self.db.session.rollback()
|
||||
error_msg = str(e)
|
||||
self.logger.error(f"Ошибка при изменении статуса региона: {error_msg}")
|
||||
self.auditlog.regions(
|
||||
action_type="toggle",
|
||||
actor_display_name=user.display_name,
|
||||
ldap_user_id=user.id,
|
||||
region_id=region_id,
|
||||
active=new_status,
|
||||
error=error_msg
|
||||
)
|
||||
return {'status': 'error', 'message': error_msg}, 500
|
||||
|
||||
|
||||
|
||||
def update_region_name(self, data, user):
|
||||
region_id = data.get('region_id')
|
||||
name = data.get('name')
|
||||
|
||||
self.logger.info(f"Изменение названия региона: region_id={region_id}, name={name}")
|
||||
|
||||
try:
|
||||
region = Regions.query.get(region_id)
|
||||
if region:
|
||||
old_name = region.region_name
|
||||
region.region_name = name
|
||||
self.db.session.commit()
|
||||
|
||||
self.logger.info(f"Название региона {region_id} изменено с {old_name} на {name}")
|
||||
self.auditlog.regions(
|
||||
action_type="rename",
|
||||
actor_display_name=user.display_name,
|
||||
ldap_user_id=user.id,
|
||||
region_id=region_id,
|
||||
new_name=name,
|
||||
old_name=old_name
|
||||
)
|
||||
|
||||
return {'status': 'success', 'message': 'Название региона изменено'}, 200
|
||||
else:
|
||||
error_msg = f"Регион с ID {region_id} не найден"
|
||||
self.logger.warning(error_msg)
|
||||
self.auditlog.regions(
|
||||
action_type="rename",
|
||||
actor_display_name=user.display_name,
|
||||
ldap_user_id=user.id,
|
||||
region_id=region_id,
|
||||
new_name=name,
|
||||
error=error_msg
|
||||
)
|
||||
return {'status': 'error', 'message': 'Регион не найден'}, 404
|
||||
except Exception as e:
|
||||
self.db.session.rollback()
|
||||
error_msg = str(e)
|
||||
self.logger.error(f"Ошибка при изменении названия региона: {error_msg}")
|
||||
self.auditlog.regions(
|
||||
action_type="rename",
|
||||
actor_display_name=user.display_name,
|
||||
ldap_user_id=user.id,
|
||||
region_id=region_id,
|
||||
new_name=name,
|
||||
error=error_msg
|
||||
)
|
||||
return {'status': 'error', 'message': error_msg}, 500
|
||||
|
||||
def delete_region(self, region_id, user):
|
||||
self.logger.info(f"Удаление региона: region_id={region_id}")
|
||||
|
||||
try:
|
||||
region = Regions.query.get(region_id)
|
||||
if region:
|
||||
name = region.region_name
|
||||
self.db.session.delete(region)
|
||||
self.db.session.commit()
|
||||
|
||||
self.logger.info(f"Регион {region_id} успешно удалён")
|
||||
self.auditlog.regions(
|
||||
action_type="delete",
|
||||
actor_display_name=user.display_name,
|
||||
ldap_user_id=user.id,
|
||||
region_id=region_id,
|
||||
new_name=name
|
||||
)
|
||||
|
||||
return {'status': 'success', 'message': 'Регион удалён'}, 200
|
||||
else:
|
||||
error_msg = f"Регион с ID {region_id} не найден"
|
||||
self.logger.warning(error_msg)
|
||||
self.auditlog.regions(
|
||||
action_type="delete",
|
||||
actor_display_name=user.display_name,
|
||||
ldap_user_id=user.id,
|
||||
region_id=region_id,
|
||||
error=error_msg
|
||||
)
|
||||
return {'status': 'error', 'message': 'Регион не найден'}, 404
|
||||
except Exception as e:
|
||||
self.db.session.rollback()
|
||||
error_msg = str(e)
|
||||
self.logger.error(f"Ошибка при удалении региона: {error_msg}")
|
||||
self.auditlog.regions(
|
||||
action_type="delete",
|
||||
actor_display_name=user.display_name,
|
||||
ldap_user_id=user.id,
|
||||
region_id=region_id,
|
||||
error=error_msg
|
||||
)
|
||||
return {'status': 'error', 'message': error_msg}, 500
|
||||
@ -1,171 +0,0 @@
|
||||
|
||||
from app import AuditLogger
|
||||
from app.models import Systems
|
||||
from app.extensions.db import db
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import asc, desc
|
||||
|
||||
auditlog = AuditLogger(db.session)
|
||||
|
||||
class SystemService:
|
||||
def __init__(self):
|
||||
self.auditlog = auditlog
|
||||
|
||||
|
||||
def get_systems(self, page=1, per_page=10, sort_field='system_id', sort_order='asc'):
|
||||
"""
|
||||
|
||||
:param page:
|
||||
:param per_page:
|
||||
:param sort_field:
|
||||
:param sort_order:
|
||||
:return:
|
||||
"""
|
||||
sort_func = asc if sort_order == 'asc' else desc
|
||||
sort_attr = getattr(Systems, sort_field, Systems.system_id)
|
||||
|
||||
if sort_field == 'system_id':
|
||||
query = Systems.query.order_by(sort_func(Systems.system_id.cast(db.Integer)))
|
||||
else:
|
||||
query = Systems.query.order_by(sort_func(sort_attr))
|
||||
|
||||
systems_query = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
|
||||
return {
|
||||
'systems': [{
|
||||
'system_id': s.system_id,
|
||||
'system_name': s.system_name,
|
||||
'name': s.name,
|
||||
} for s in systems_query.items],
|
||||
'total_systems': systems_query.total,
|
||||
'total_pages': systems_query.pages,
|
||||
'current_page': systems_query.page,
|
||||
'per_page': systems_query.per_page
|
||||
}
|
||||
|
||||
def get_system_by_id(self, system_id):
|
||||
"""
|
||||
|
||||
:param system_id:
|
||||
:return:
|
||||
"""
|
||||
return Systems.query.filter_by(system_id=system_id).first()
|
||||
|
||||
def add_system(self, data):
|
||||
"""
|
||||
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
system_id = data.get('system_id')
|
||||
system_name = data.get('system_name')
|
||||
name = data.get('name')
|
||||
error = None
|
||||
|
||||
if not system_id.isdigit():
|
||||
error = 'ID системы должен содержать только числа.'
|
||||
status = 400
|
||||
elif Systems.query.get(system_id):
|
||||
error = 'Система с таким ID уже существует'
|
||||
status = 409
|
||||
else:
|
||||
try:
|
||||
system = Systems(system_id=system_id, system_name=system_name, name=name)
|
||||
db.session.add(system)
|
||||
db.session.commit()
|
||||
status = 201
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
error = str(e)
|
||||
status = 500
|
||||
|
||||
self.auditlog.systems(
|
||||
action_type="add",
|
||||
actor_display_name=current_user.display_name,
|
||||
ldap_user_id=current_user.id,
|
||||
system_id=system_id,
|
||||
name=f'{system_name}; ({name})',
|
||||
error=error
|
||||
)
|
||||
|
||||
return {'status': 'error' if error else 'success', 'message': error or 'Система добавлена'}, status
|
||||
|
||||
def update_system_name(self, data):
|
||||
system_id = data.get('system_id')
|
||||
system_name = data.get('system_name')
|
||||
name = data.get('name')
|
||||
error = None
|
||||
|
||||
if system_id is None:
|
||||
return {'status': 'error', 'message': 'system_id обязателен'}, 400
|
||||
if system_name is None or name is None:
|
||||
return {'status': 'error', 'message': 'Поля system_name и name обязательны'}, 400
|
||||
|
||||
try:
|
||||
system = Systems.query.get(system_id)
|
||||
if system:
|
||||
old_name = system.name # Старое имя
|
||||
system.system_name = system_name
|
||||
system.name = name
|
||||
db.session.commit()
|
||||
status = 200
|
||||
else:
|
||||
error = 'Система не найдена'
|
||||
status = 404
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
error = str(e)
|
||||
status = 500
|
||||
|
||||
# Формируем отображение вида "старое_имя → новое_имя"
|
||||
if not error and system:
|
||||
log_name = f"{old_name} → {name}"
|
||||
else:
|
||||
log_name = name # Если ошибка, логируем только новое имя
|
||||
|
||||
self.auditlog.systems(
|
||||
action_type="update",
|
||||
actor_display_name=current_user.display_name,
|
||||
ldap_user_id=current_user.id,
|
||||
system_id=system_id,
|
||||
name=log_name,
|
||||
error=error
|
||||
)
|
||||
|
||||
return {'status': 'error' if error else 'success', 'message': error or 'Название системы изменено'}, status
|
||||
|
||||
|
||||
|
||||
|
||||
def delete_system(self, system_id):
|
||||
"""
|
||||
|
||||
:param system_id:
|
||||
:return:
|
||||
"""
|
||||
error = None
|
||||
system = Systems.query.get(system_id)
|
||||
|
||||
if not system:
|
||||
error = 'Система не найдена'
|
||||
status = 404
|
||||
else:
|
||||
try:
|
||||
db.session.delete(system)
|
||||
db.session.commit()
|
||||
status = 200
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
error = str(e)
|
||||
status = 500
|
||||
|
||||
self.auditlog.systems(
|
||||
action_type="delete",
|
||||
actor_display_name=current_user.display_name,
|
||||
ldap_user_id=current_user.id,
|
||||
system_id=system_id,
|
||||
name=f'{system.system_name}; ({system.name})' if system else None,
|
||||
error=error
|
||||
)
|
||||
|
||||
return {'status': 'error' if error else 'success', 'message': error or 'Система удалена'}, status
|
||||
@ -1,37 +0,0 @@
|
||||
# app/services/user_event_service.py
|
||||
|
||||
from typing import Dict, Any, Tuple
|
||||
from app.extensions.db import db
|
||||
from app.models import UserEvents, Users # Импортируем модель Users для получения telegram_id
|
||||
|
||||
|
||||
def log_user_action(chat_id: int, action: str) -> Tuple[Dict[str, str], int]:
|
||||
try:
|
||||
# Получаем telegram_id пользователя по chat_id
|
||||
user = Users.query.filter_by(chat_id=chat_id).first()
|
||||
if not user:
|
||||
return {'error': f'Пользователь с chat_id {chat_id} не найден для логирования действия.'}, 404
|
||||
|
||||
new_event = UserEvents(
|
||||
chat_id=chat_id,
|
||||
telegram_id=user.telegram_id, # Добавляем telegram_id из найденного пользователя
|
||||
action=action,
|
||||
# timestamp генерируется автоматически по default=db.func.current_timestamp()
|
||||
)
|
||||
db.session.add(new_event)
|
||||
db.session.commit()
|
||||
return {'message': 'Действие сохранено'}, 200
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return {'error': str(e)}, 500
|
||||
|
||||
def get_user_events(chat_id: int) -> tuple[list[dict[str, Any | None]], int]:
|
||||
# Предполагаем, что у UserEvents есть поля 'id' и 'timestamp'
|
||||
events = UserEvents.query.filter_by(chat_id=chat_id).order_by(UserEvents.timestamp.desc()).all()
|
||||
events_list = [{
|
||||
'event_id': e.id,
|
||||
'chat_id': e.chat_id,
|
||||
'event_type': e.action, # Используем 'action', как в модели UserEvents
|
||||
'timestamp': e.timestamp.isoformat() if e.timestamp else None # Форматируем дату
|
||||
} for e in events]
|
||||
return events_list, 200
|
||||
@ -1,232 +0,0 @@
|
||||
# app/services/user_service.py
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Dict, List, Optional, Tuple, Any
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy import or_
|
||||
|
||||
from app import db
|
||||
from app.models import Users # Предполагаем, что app.models/__init__.py экспортирует Users
|
||||
from app.extensions.audit_logger import AuditLogger
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
auditlog = AuditLogger(db.session)
|
||||
|
||||
def get_users(page: int, per_page: int) -> Dict[str, Any]:
|
||||
logger.debug(f"Получение пользователей: page={page}, per_page={per_page}")
|
||||
users_query = Users.query.options(joinedload(Users.subscriptions))
|
||||
users_paginated = users_query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
|
||||
users_list: List[Dict[str, Any]] = []
|
||||
for user in users_paginated.items:
|
||||
user_data: Dict[str, Any] = {
|
||||
'chat_id': user.chat_id,
|
||||
'telegram_id': user.telegram_id,
|
||||
'email': user.user_email,
|
||||
'subscriptions': [],
|
||||
'disaster_only': "Все уведомления",
|
||||
'status': "Активен" if not user.is_blocked else "Заблокирован",
|
||||
'blocked': user.is_blocked
|
||||
}
|
||||
|
||||
if user.subscriptions:
|
||||
for subscription in user.subscriptions:
|
||||
if subscription.active and not subscription.skip:
|
||||
user_data['subscriptions'].append(subscription.region_id)
|
||||
if subscription.disaster_only:
|
||||
user_data['disaster_only'] = "Только критические уведомления"
|
||||
|
||||
users_list.append(user_data)
|
||||
logger.debug(f"Получено пользователей: {len(users_list)} элементов")
|
||||
return {
|
||||
'users': users_list,
|
||||
'total_users': users_paginated.total,
|
||||
'total_pages': users_paginated.pages,
|
||||
'current_page': users_paginated.page,
|
||||
'per_page': users_paginated.per_page
|
||||
}
|
||||
|
||||
def get_user(chat_id: int) -> Optional[Dict[str, Any]]:
|
||||
logger.debug(f"Получение пользователя: chat_id={chat_id}")
|
||||
user: Optional[Users] = Users.query.filter_by(chat_id=chat_id).first()
|
||||
if user:
|
||||
user_data: Dict[str, Any] = {
|
||||
'chat_id': user.chat_id,
|
||||
'telegram_id': user.telegram_id,
|
||||
'email': user.user_email,
|
||||
'blocked': user.is_blocked
|
||||
}
|
||||
logger.debug(f"Пользователь найден: chat_id={chat_id}")
|
||||
return user_data
|
||||
else:
|
||||
logger.warning(f"Пользователь не найден: chat_id={chat_id}")
|
||||
return None
|
||||
|
||||
def toggle_block_user(chat_id: int, actor_user: Any) -> Tuple[Dict[str, Any], int]:
|
||||
logger.debug(f"Переключение блокировки пользователя: chat_id={chat_id}")
|
||||
user: Optional[Users] = Users.query.filter_by(chat_id=chat_id).first()
|
||||
if not user:
|
||||
error_msg = "Пользователь не найден"
|
||||
auditlog.users(action_type="toggle_block", actor_display_name=actor_user.display_name,
|
||||
ldap_user_id=actor_user.id, affected_chat_id=chat_id, error=error_msg)
|
||||
logger.warning(f"{error_msg}: chat_id={chat_id}")
|
||||
return {'status': 'error', 'message': error_msg}, 404
|
||||
try:
|
||||
user.is_blocked = not user.is_blocked
|
||||
db.session.commit()
|
||||
status_text = "заблокирован" if user.is_blocked else "разблокирован"
|
||||
logger.info(f"Пользователь {chat_id} {status_text}")
|
||||
|
||||
action_type = "block" if user.is_blocked else "unblock"
|
||||
auditlog.users(
|
||||
action_type=action_type,
|
||||
actor_display_name=actor_user.display_name,
|
||||
ldap_user_id=actor_user.id,
|
||||
affected_chat_id=chat_id,
|
||||
email=user.user_email,
|
||||
telegram_id=user.telegram_id,
|
||||
)
|
||||
return {'status': 'updated', 'new_status': user.is_blocked}, 200
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
error_msg = str(e)
|
||||
logger.error(f"Ошибка при переключении блокировки пользователя {chat_id}: {error_msg}")
|
||||
auditlog.users(action_type="toggle_block", actor_display_name=actor_user.display_name,
|
||||
ldap_user_id=actor_user.id, affected_chat_id=chat_id, error=error_msg)
|
||||
return {'status': 'error', 'message': error_msg}, 500
|
||||
|
||||
|
||||
def delete_user(chat_id: int, actor_user: Any) -> Tuple[Dict[str, Any], int]:
|
||||
logger.info(f"Удаление пользователя: chat_id={chat_id}")
|
||||
user: Optional[Users] = Users.query.filter_by(chat_id=chat_id).first()
|
||||
if not user:
|
||||
error_msg = "Пользователь не найден"
|
||||
auditlog.users(action_type="delete", actor_display_name=actor_user.display_name,
|
||||
ldap_user_id=actor_user.id, affected_chat_id=chat_id, error=error_msg)
|
||||
logger.warning(f"{error_msg}: chat_id={chat_id}")
|
||||
return {'status': 'error', 'message': error_msg}, 404
|
||||
try:
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
logger.info(f"Пользователь удален: chat_id={chat_id}")
|
||||
auditlog.users(
|
||||
action_type="delete",
|
||||
actor_display_name=actor_user.display_name,
|
||||
ldap_user_id=actor_user.id,
|
||||
affected_chat_id=chat_id,
|
||||
email=user.user_email,
|
||||
telegram_id=user.telegram_id,
|
||||
)
|
||||
return {'status': 'deleted', 'message': 'Пользователь удален'}, 200
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
error_msg = str(e)
|
||||
logger.error(f"Ошибка при удалении пользователя {chat_id}: {error_msg}")
|
||||
auditlog.users(action_type="delete", actor_display_name=actor_user.display_name,
|
||||
ldap_user_id=actor_user.id, affected_chat_id=chat_id, error=error_msg)
|
||||
return {'status': 'error', 'message': error_msg}, 500
|
||||
|
||||
def add_user(user_data: Dict[str, Any], actor_user: Any) -> Tuple[Dict[str, str], int]:
|
||||
logger.info(f"Добавление пользователя: {user_data}")
|
||||
chat_id = None
|
||||
telegram_id = user_data.get('telegram_id')
|
||||
user_email = user_data.get('user_email')
|
||||
|
||||
try:
|
||||
try:
|
||||
chat_id = int(user_data.get('chat_id'))
|
||||
except (ValueError, TypeError):
|
||||
error_msg = 'Chat ID должен быть числом'
|
||||
auditlog.users(action_type="add", actor_display_name=actor_user.display_name,
|
||||
ldap_user_id=actor_user.id, affected_chat_id=chat_id,
|
||||
telegram_id=telegram_id, email=user_email, error=error_msg)
|
||||
logger.warning(error_msg)
|
||||
return {'error': error_msg}, 400
|
||||
|
||||
if not telegram_id or not re.match(r'^@.*$', telegram_id):
|
||||
error_msg = "Telegram ID должен начинаться с символа @"
|
||||
auditlog.users(action_type="add", actor_display_name=actor_user.display_name,
|
||||
ldap_user_id=actor_user.id, affected_chat_id=chat_id,
|
||||
telegram_id=telegram_id, email=user_email, error=error_msg)
|
||||
logger.warning(error_msg)
|
||||
return {'error': error_msg}, 400
|
||||
if not user_email or not re.match(r'.*@rtmis.ru$', user_email):
|
||||
error_msg = "Email должен содержать домен @rtmis.ru"
|
||||
auditlog.users(action_type="add", actor_display_name=actor_user.display_name,
|
||||
ldap_user_id=actor_user.id, affected_chat_id=chat_id,
|
||||
telegram_id=telegram_id, email=user_email, error=error_msg)
|
||||
logger.warning(error_msg)
|
||||
return {'error': error_msg}, 400
|
||||
|
||||
existing_user = Users.query.filter(
|
||||
(Users.user_email == user_email) |
|
||||
(Users.telegram_id == telegram_id) |
|
||||
(Users.chat_id == chat_id)
|
||||
).first()
|
||||
|
||||
if existing_user:
|
||||
error_msg = 'Пользователь с таким Chat ID, Telegram ID или Email уже существует.'
|
||||
auditlog.users(action_type="add", actor_display_name=actor_user.display_name,
|
||||
ldap_user_id=actor_user.id, affected_chat_id=chat_id,
|
||||
telegram_id=telegram_id, email=user_email, error=error_msg)
|
||||
logger.warning(error_msg)
|
||||
return {'error': error_msg}, 409
|
||||
|
||||
new_user: Users = Users(
|
||||
chat_id=chat_id,
|
||||
telegram_id=telegram_id,
|
||||
user_email=user_email,
|
||||
is_blocked=user_data.get('is_blocked', False)
|
||||
)
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
logger.info(f"Пользователь добавлен успешно: {new_user.user_email}")
|
||||
auditlog.users(action_type="add", actor_display_name=actor_user.display_name,
|
||||
ldap_user_id=actor_user.id, affected_chat_id=chat_id,
|
||||
telegram_id=telegram_id, email=user_email)
|
||||
return {'message': 'Пользователь добавлен успешно'}, 201
|
||||
except IntegrityError as e:
|
||||
db.session.rollback()
|
||||
error_msg = 'Ошибка уникальности данных'
|
||||
logger.error(f"Ошибка уникальности при добавлении пользователя: {e}")
|
||||
auditlog.users(action_type="add", actor_display_name=actor_user.display_name,
|
||||
ldap_user_id=actor_user.id, affected_chat_id=chat_id,
|
||||
telegram_id=telegram_id, email=user_email, error=error_msg)
|
||||
return {'error': error_msg}, 409
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
error_msg = f'Ошибка при добавлении пользователя: {type(e).__name__}: {e}'
|
||||
logger.error(error_msg)
|
||||
auditlog.users(action_type="add", actor_display_name=actor_user.display_name,
|
||||
ldap_user_id=actor_user.id, affected_chat_id=chat_id,
|
||||
telegram_id=telegram_id, email=user_email, error=error_msg)
|
||||
return {'error': 'Ошибка при добавлении пользователя'}, 500
|
||||
|
||||
def search_users(telegram_id: Optional[str] = None, email: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
logger.debug(f"Поиск пользователей: telegram_id={telegram_id}, email={email}")
|
||||
|
||||
query = db.session.query(Users)
|
||||
if telegram_id:
|
||||
query = query.filter(Users.telegram_id.ilike(f"%{telegram_id}%"))
|
||||
if email:
|
||||
query = query.filter(Users.user_email.ilike(f"%{email}%"))
|
||||
|
||||
users: List[Users] = query.all()
|
||||
|
||||
users_list: List[Dict[str, Any]] = []
|
||||
for user in users:
|
||||
# Используем названия полей модели напрямую
|
||||
user_data: Dict[str, Any] = {
|
||||
'chat_id': user.chat_id,
|
||||
'telegram_id': user.telegram_id,
|
||||
'email': user.user_email,
|
||||
'blocked': user.is_blocked
|
||||
}
|
||||
users_list.append(user_data)
|
||||
|
||||
logger.debug(f"Найдено пользователей: {len(users_list)}")
|
||||
return users_list
|
||||
@ -1,4 +0,0 @@
|
||||
/* Курсор "рука" для иконки календаря (для Chrome и поддерживаемых браузеров) */
|
||||
input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
body {
|
||||
background-color: #f4f6f9;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
border-color: #004085;
|
||||
}
|
||||
@ -1,649 +0,0 @@
|
||||
|
||||
|
||||
const perPage=10
|
||||
let currentPage=1
|
||||
let totalPages=1
|
||||
let regionsCurrentPage=1
|
||||
let sortField="region_id"
|
||||
let sortOrder="asc"
|
||||
let currentFetchController=null
|
||||
let currentSystemIdToDelete=null
|
||||
let systemsCurrentPage=1
|
||||
let systemsTotalPages=1
|
||||
let systemsSortField="system_id"
|
||||
let systemsSortOrder="asc"
|
||||
|
||||
|
||||
// Предварительно создаём объекты модалей (чтобы не создавать каждый раз заново)
|
||||
const editRegionNameModal = new bootstrap.Modal(document.getElementById('editRegionNameModal'));
|
||||
const deleteRegionModal = new bootstrap.Modal(document.getElementById('deleteRegionModal'));
|
||||
const regionSubscribersModal = new bootstrap.Modal(document.getElementById('regionSubscribersModal'));
|
||||
|
||||
const editSystemNameModal = new bootstrap.Modal(document.getElementById('editSystemNameModal'));
|
||||
const deleteSystemModal = new bootstrap.Modal(document.getElementById('deleteSystemModal'));
|
||||
|
||||
|
||||
function loadRegions(e){
|
||||
if(e<1||e>totalPages)return;
|
||||
currentPage=e;
|
||||
let t=`/telezab/rest/api/regions?page=${currentPage}&per_page=${perPage}&sort_field=${sortField}&sort_order=${sortOrder}`;
|
||||
currentFetchController&¤tFetchController.abort(),
|
||||
safeFetch(t,{signal:(currentFetchController=new AbortController).signal})
|
||||
.then(e=>e.json())
|
||||
.then(e=>{
|
||||
currentFetchController=null,
|
||||
totalPages=e.total_pages,
|
||||
updateRegionsTable(e.regions),
|
||||
updatePagination(e.current_page,e.total_pages,"pagination-regions")
|
||||
})
|
||||
.catch(e=>{
|
||||
"AbortError"===e.name||console.error("Error fetching regions:",e),
|
||||
currentFetchController=null
|
||||
})
|
||||
}
|
||||
|
||||
function updateRegionsTable(e){
|
||||
let t=document.getElementById("regions-table");
|
||||
if(!t) {
|
||||
console.error("regions-table element not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
t.innerHTML="";
|
||||
e.forEach(e=>{
|
||||
let n=document.createElement("tr");
|
||||
n.innerHTML=`
|
||||
<td>${e.region_id}</td>
|
||||
<td>${e.name}</td>
|
||||
<td>
|
||||
<span id="region-status-label-${e.region_id}">${e.active ? "Включен" : "Выключен"}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="form-check form-switch me-2">
|
||||
<input class="form-check-input region-status-switch" type="checkbox" role="switch" id="region-status-${e.region_id}" data-id="${e.region_id}" ${e.active ? "checked" : ""}>
|
||||
<label class="form-check-label" for="region-status-${e.region_id}"></label>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary edit-name-btn me-2" data-id="${e.region_id}" data-name="${e.name}" title="Редактировать название региона">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-info subscribers-btn me-2" data-id="${e.region_id}" title="Посмотреть подписчиков региона">
|
||||
<i class="bi bi-people"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger delete-btn" data-id="${e.region_id}" title="Удалить регион">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
t.appendChild(n)
|
||||
});
|
||||
setupRegionActions();
|
||||
}
|
||||
|
||||
function setupRegionActions(){
|
||||
// Используем делегирование событий для избежания множественных обработчиков
|
||||
const regionsTable = document.getElementById("regions-table");
|
||||
if (!regionsTable) return;
|
||||
|
||||
// Удаляем старые обработчики событий с таблицы
|
||||
const newTable = regionsTable.cloneNode(true);
|
||||
regionsTable.parentNode.replaceChild(newTable, regionsTable);
|
||||
|
||||
// Используем делегирование событий
|
||||
newTable.addEventListener('click', function(e) {
|
||||
const target = e.target.closest('button');
|
||||
if (!target) return;
|
||||
|
||||
if (target.classList.contains('delete-btn')) {
|
||||
e.preventDefault();
|
||||
deleteRegion(target.dataset.id);
|
||||
} else if (target.classList.contains('edit-name-btn')) {
|
||||
e.preventDefault();
|
||||
handleEditRegionName(target);
|
||||
} else if (target.classList.contains('subscribers-btn')) {
|
||||
e.preventDefault();
|
||||
showRegionSubscribers(target.dataset.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчик для переключателей статуса
|
||||
newTable.addEventListener('change', function(e) {
|
||||
if (e.target.classList.contains('region-status-switch')) {
|
||||
let id = e.target.dataset.id;
|
||||
let active = e.target.checked;
|
||||
toggleRegionStatus(id, active);
|
||||
document.getElementById(`region-status-label-${id}`).textContent = active ? "Включен" : "Выключен";
|
||||
}
|
||||
});
|
||||
|
||||
// Обновляем заголовки для сортировки
|
||||
document.querySelectorAll("th[data-sort]").forEach(e=>{
|
||||
const newHeader = e.cloneNode(true);
|
||||
e.parentNode.replaceChild(newHeader, e);
|
||||
newHeader.addEventListener("click", ()=>{
|
||||
let field = newHeader.dataset.sort;
|
||||
if(field === sortField) {
|
||||
sortOrder = sortOrder === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
sortField = field;
|
||||
sortOrder = "asc";
|
||||
}
|
||||
loadRegions(currentPage);
|
||||
});
|
||||
});
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[title]'));
|
||||
tooltipTriggerList.map(el => new bootstrap.Tooltip(el));
|
||||
}
|
||||
|
||||
function handleEditRegionName(e){
|
||||
let t = e.dataset.id,
|
||||
n = e.dataset.name;
|
||||
document.getElementById("old-region-name").value = n;
|
||||
document.getElementById("new-region-name").value = n;
|
||||
|
||||
editRegionNameModal.show();
|
||||
|
||||
let s = 5;
|
||||
document.getElementById("edit-region-name-timer").textContent = s;
|
||||
let a = setInterval(() => {
|
||||
s--;
|
||||
document.getElementById("edit-region-name-timer").textContent = s;
|
||||
if (s === 0) {
|
||||
clearInterval(a);
|
||||
document.getElementById("save-region-name-btn").removeAttribute("disabled");
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
let r = document.getElementById("save-region-name-btn"),
|
||||
o = r.cloneNode(true);
|
||||
r.parentNode.replaceChild(o, r);
|
||||
o.addEventListener("click", () => {
|
||||
updateRegionName(t, document.getElementById("new-region-name").value);
|
||||
editRegionNameModal.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function showRegionSubscribers(e){
|
||||
safeFetch(`/telezab/rest/api/regions/${e}/subscribers`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
let t = document.getElementById("regionSubscribersTableBody");
|
||||
t.innerHTML = "";
|
||||
if(data.subscribers && data.subscribers.length > 0){
|
||||
data.subscribers.forEach(sub => {
|
||||
let n = document.createElement("tr");
|
||||
n.innerHTML = `
|
||||
<td>${sub.telegram_id}</td>
|
||||
<td>${sub.email}</td>
|
||||
`;
|
||||
t.appendChild(n);
|
||||
});
|
||||
} else {
|
||||
let n = document.createElement("tr");
|
||||
n.innerHTML = `<td colspan="2">Нет подписчиков для этого региона.</td>`;
|
||||
t.appendChild(n);
|
||||
}
|
||||
regionSubscribersModal.show();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Ошибка при получении подписчиков региона:", err);
|
||||
toastr.error("Ошибка при получении подписчиков региона. Пожалуйста, попробуйте позже.");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function updateRegionName(regionId, newName) {
|
||||
safeFetch("/telezab/rest/api/regions/name", {
|
||||
method:"PUT",
|
||||
headers:{"Content-Type":"application/json"},
|
||||
body:JSON.stringify({region_id:regionId, name:newName})
|
||||
})
|
||||
.then(()=>{
|
||||
loadRegions(currentPage);
|
||||
toastr.success("Название региона изменено.");
|
||||
})
|
||||
.catch(e=>{
|
||||
console.error("Ошибка при изменении названия региона:",e);
|
||||
toastr.error("Ошибка при изменении названия региона. Пожалуйста, попробуйте позже.");
|
||||
});
|
||||
}
|
||||
|
||||
function toggleRegionStatus(e,t){
|
||||
safeFetch("/telezab/rest/api/regions/status",{
|
||||
method:"PUT",
|
||||
headers:{"Content-Type":"application/json"},
|
||||
body:JSON.stringify({region_id:e,active:t})
|
||||
})
|
||||
.then(()=>{
|
||||
loadRegions(currentPage);
|
||||
t?toastr.success("Регион активирован."):toastr.success("Регион деактивирован.");
|
||||
})
|
||||
.catch(e=>{
|
||||
console.error("Ошибка при изменении статуса региона:",e);
|
||||
toastr.error("Ошибка при изменении статуса региона. Пожалуйста, попробуйте позже.");
|
||||
});
|
||||
}
|
||||
|
||||
function deleteRegion(e){
|
||||
deleteRegionModal.show();
|
||||
|
||||
let t = document.getElementById("deleteConfirmationInput"),
|
||||
n = document.getElementById("confirmDeleteButton"),
|
||||
s = document.getElementById("deleteRegionModal"),
|
||||
a = t.cloneNode(true),
|
||||
r = n.cloneNode(true);
|
||||
|
||||
t.parentNode.replaceChild(a, t);
|
||||
n.parentNode.replaceChild(r, n);
|
||||
|
||||
a.addEventListener("input", function(){
|
||||
let val = this.value;
|
||||
r.disabled = val === "УДАЛИТЬ" ? false : true;
|
||||
});
|
||||
|
||||
r.addEventListener("click", function(){
|
||||
safeFetch(`/telezab/rest/api/regions/${e}`, { method: "DELETE" })
|
||||
.then(() => {
|
||||
loadRegions(currentPage);
|
||||
toastr.success("Регион успешно удален.");
|
||||
deleteRegionModal.hide();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Ошибка при удалении региона:", err);
|
||||
toastr.error("Ошибка при удалении региона. Пожалуйста, попробуйте позже.");
|
||||
});
|
||||
});
|
||||
|
||||
s.addEventListener("hidden.bs.modal", function(){
|
||||
a.value = "";
|
||||
r.disabled = true;
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
function loadSystems(e){
|
||||
if(e<1||e>systemsTotalPages)return;
|
||||
systemsCurrentPage=e;
|
||||
let t=`/telezab/rest/api/systems?page=${systemsCurrentPage}&per_page=10&sort_field=${systemsSortField}&sort_order=${systemsSortOrder}`;
|
||||
currentFetchController&¤tFetchController.abort(),
|
||||
safeFetch(t,{signal:(currentFetchController=new AbortController).signal})
|
||||
.then(e=>e.json())
|
||||
.then(e=>{
|
||||
currentFetchController=null,
|
||||
systemsTotalPages=e.total_pages,
|
||||
updateSystemsTable(e.systems),
|
||||
updatePagination(e.current_page,e.total_pages,"pagination-systems")
|
||||
})
|
||||
.catch(e=>{
|
||||
"AbortError"===e.name||console.error("Error fetching systems:",e),
|
||||
currentFetchController=null
|
||||
});
|
||||
}
|
||||
|
||||
function updateSystemsTable(e) {
|
||||
let t = document.getElementById("systems-table");
|
||||
if (!t) {
|
||||
console.error("systems-table element not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
t.innerHTML = "";
|
||||
e.forEach(e => {
|
||||
let n = document.createElement("tr");
|
||||
n.innerHTML = `
|
||||
<td>${e.system_id}</td>
|
||||
<td>${e.system_name}</td>
|
||||
<td>${e.name}</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<button class="btn btn-sm btn-primary edit-name-btn me-2"
|
||||
data-id="${e.system_id}"
|
||||
data-latin-name="${e.system_name}"
|
||||
data-name="${e.name}"
|
||||
title="Редактировать название системы">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger delete-btn"
|
||||
data-id="${e.system_id}"
|
||||
title="Удалить систему">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
t.appendChild(n);
|
||||
});
|
||||
|
||||
setupSystemActions();
|
||||
|
||||
// Инициализация Bootstrap tooltips
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[title]'));
|
||||
tooltipTriggerList.map(el => new bootstrap.Tooltip(el));
|
||||
}
|
||||
|
||||
function setupSystemActions(){
|
||||
// Используем делегирование событий для таблицы систем
|
||||
const systemsTable = document.getElementById("systems-table");
|
||||
if (!systemsTable) return;
|
||||
|
||||
// Удаляем старые обработчики событий с таблицы
|
||||
const newTable = systemsTable.cloneNode(true);
|
||||
systemsTable.parentNode.replaceChild(newTable, systemsTable);
|
||||
|
||||
// Используем делегирование событий
|
||||
newTable.addEventListener('click', function(e) {
|
||||
const target = e.target.closest('button');
|
||||
if (!target) return;
|
||||
|
||||
if (target.classList.contains('delete-btn')) {
|
||||
e.preventDefault();
|
||||
deleteSystem(target.dataset.id);
|
||||
} else if (target.classList.contains('edit-name-btn')) {
|
||||
e.preventDefault();
|
||||
handleEditSystemName(target);
|
||||
}
|
||||
});
|
||||
|
||||
// Обновляем заголовки для сортировки
|
||||
document.querySelectorAll("th[data-sort]").forEach(e=>{
|
||||
const newHeader = e.cloneNode(true);
|
||||
e.parentNode.replaceChild(newHeader, e);
|
||||
newHeader.addEventListener("click", ()=>{
|
||||
let field = newHeader.dataset.sort;
|
||||
if(field === systemsSortField) {
|
||||
systemsSortOrder = systemsSortOrder === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
systemsSortField = field;
|
||||
systemsSortOrder = "asc";
|
||||
}
|
||||
loadSystems(systemsCurrentPage);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleEditSystemName(e){
|
||||
let t = e.dataset.id,
|
||||
n = e.dataset.latinName,
|
||||
s = e.dataset.name;
|
||||
|
||||
let a = document.getElementById("edit-system-id"),
|
||||
r = document.getElementById("edit-system-name-lat"),
|
||||
o = document.getElementById("old-system-name"),
|
||||
l = document.getElementById("new-system-name");
|
||||
|
||||
if(!a || !r || !o || !l){
|
||||
console.error("❌ Один или несколько элементов не найдены:");
|
||||
!a && console.error(" - Не найден элемент #edit-system-id");
|
||||
!r && console.error(" - Не найден элемент #edit-system-name-lat");
|
||||
!o && console.error(" - Не найден элемент #old-system-name");
|
||||
!l && console.error(" - Не найден элемент #new-system-name");
|
||||
return;
|
||||
}
|
||||
|
||||
a.value = t;
|
||||
r.value = n;
|
||||
o.value = s;
|
||||
l.value = s;
|
||||
|
||||
editSystemNameModal.show();
|
||||
|
||||
let d = 5,
|
||||
i = document.getElementById("edit-system-name-timer");
|
||||
i.textContent = d;
|
||||
|
||||
let m = setInterval(() => {
|
||||
d--;
|
||||
i.textContent = d;
|
||||
if(d === 0){
|
||||
clearInterval(m);
|
||||
document.getElementById("saveSystemNameBtn").removeAttribute("disabled");
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
let c = document.getElementById("saveSystemNameBtn"),
|
||||
g = c.cloneNode(true);
|
||||
c.parentNode.replaceChild(g, c);
|
||||
|
||||
g.addEventListener("click", () => {
|
||||
let id = a.value,
|
||||
latinName = r.value,
|
||||
newName = l.value.trim();
|
||||
|
||||
if(!newName){
|
||||
toastr.error("Введите новое название системы.");
|
||||
return;
|
||||
}
|
||||
|
||||
updateSystemName(id, latinName, newName);
|
||||
editSystemNameModal.hide();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function updateSystemName(systemId, systemNameLat, nameCyr) {
|
||||
if (!systemId || !systemNameLat || !nameCyr) {
|
||||
toastr.error("Не все параметры заполнены.");
|
||||
return;
|
||||
}
|
||||
|
||||
safeFetch("/telezab/rest/api/systems", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
system_id: systemId,
|
||||
system_name: systemNameLat,
|
||||
name: nameCyr
|
||||
})
|
||||
})
|
||||
.then(res => res.ok ? res.json() : res.json().then(e => { throw new Error(e.message || "Ошибка сервера") }))
|
||||
.then(() => {
|
||||
loadSystems(systemsCurrentPage);
|
||||
toastr.success("Название системы успешно обновлено.");
|
||||
})
|
||||
.catch(e => {
|
||||
console.error("Ошибка при изменении названия системы:", e);
|
||||
toastr.error(e.message || "Ошибка при изменении названия системы. Пожалуйста, попробуйте позже.");
|
||||
});
|
||||
}
|
||||
|
||||
function deleteSystem(e){
|
||||
deleteSystemModal.show();
|
||||
|
||||
let t = document.getElementById("deleteSystemConfirmationInput"),
|
||||
n = document.getElementById("confirmDeleteSystemButton"),
|
||||
s = document.getElementById("deleteSystemModal"),
|
||||
a = t.cloneNode(true),
|
||||
r = n.cloneNode(true);
|
||||
|
||||
t.parentNode.replaceChild(a, t);
|
||||
n.parentNode.replaceChild(r, n);
|
||||
|
||||
a.addEventListener("input", function(){
|
||||
let val = this.value;
|
||||
r.disabled = val === "УДАЛИТЬ" ? false : true;
|
||||
});
|
||||
|
||||
r.addEventListener("click", function(){
|
||||
safeFetch(`/telezab/rest/api/systems/${e}`, { method: "DELETE" })
|
||||
.then(() => {
|
||||
loadSystems(systemsCurrentPage);
|
||||
toastr.success("Система успешно удалена.");
|
||||
deleteSystemModal.hide();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Ошибка при удалении системы:", err);
|
||||
toastr.error("Ошибка при удалении системы. Пожалуйста, попробуйте позже.");
|
||||
});
|
||||
});
|
||||
|
||||
s.addEventListener("hidden.bs.modal", function(){
|
||||
a.value = "";
|
||||
r.disabled = true;
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
|
||||
function updatePagination(e,t,n){
|
||||
let s=document.getElementById(n);
|
||||
if(!s){
|
||||
console.error(`Container with ID ${n} not found!`);
|
||||
return;
|
||||
}
|
||||
s.innerHTML="";
|
||||
|
||||
let a=document.createElement("li");
|
||||
a.classList.add("page-item");
|
||||
a.classList.toggle("disabled",1===e);
|
||||
a.innerHTML=`<a class="page-link" href="#" aria-label="Previous" onclick="loadPage(${e-1}, '${n}')">«</a>`;
|
||||
s.appendChild(a);
|
||||
|
||||
for(let r=1;r<=t;r++){
|
||||
let o=document.createElement("li");
|
||||
o.classList.add("page-item");
|
||||
o.classList.toggle("active",r===e);
|
||||
let l=document.createElement("a");
|
||||
l.classList.add("page-link");
|
||||
l.href="#";
|
||||
l.textContent=r;
|
||||
l.onclick=()=>loadPage(r,n);
|
||||
o.appendChild(l);
|
||||
s.appendChild(o);
|
||||
}
|
||||
|
||||
let i=document.createElement("li");
|
||||
i.classList.add("page-item");
|
||||
i.classList.toggle("disabled",e===t);
|
||||
i.innerHTML=`<a class="page-link" href="#" aria-label="Next" onclick="loadPage(${e+1}, '${n}')">»</a>`;
|
||||
s.appendChild(i);
|
||||
}
|
||||
|
||||
function loadPage(e,t){
|
||||
"pagination-regions"===t?loadRegions(e):"pagination-systems"===t&&loadSystems(e);
|
||||
}
|
||||
|
||||
// Настройка toastr
|
||||
toastr.options={
|
||||
closeButton:!0,
|
||||
debug:!1,
|
||||
newestOnTop:!1,
|
||||
progressBar:!1,
|
||||
positionClass:"toast-bottom-right",
|
||||
preventDuplicates:!1,
|
||||
onclick:null,
|
||||
showDuration:"300",
|
||||
hideDuration:"1000",
|
||||
timeOut:"5000",
|
||||
extendedTimeOut:"1000",
|
||||
showEasing:"swing",
|
||||
hideEasing:"linear",
|
||||
showMethod:"fadeIn",
|
||||
hideMethod:"fadeOut"
|
||||
};
|
||||
|
||||
// Обработчики форм
|
||||
document.getElementById("add-region-form").addEventListener("submit",e=>{
|
||||
e.preventDefault();
|
||||
let regionId=document.getElementById("region-id").value;
|
||||
let regionName=document.getElementById("region-name").value;
|
||||
let regionActive=document.getElementById("region-active").checked;
|
||||
|
||||
if(!/^\d+$/.test(regionId)){
|
||||
toastr.error("ID региона должен содержать только числа.");
|
||||
console.log("Ошибка: ID региона не является числом.");
|
||||
return;
|
||||
}
|
||||
|
||||
safeFetch("/telezab/rest/api/regions",{
|
||||
method:"POST",
|
||||
headers:{"Content-Type":"application/json"},
|
||||
body:JSON.stringify({region_id:regionId,name:regionName,active:regionActive})
|
||||
})
|
||||
.then(e=>e.ok?e.json():e.json().then(e=>{
|
||||
throw console.error("Ошибка от сервера:",e),new Error(e.message||"Ошибка добавления региона");
|
||||
}))
|
||||
.then(e=>{
|
||||
if("success"===e.status){
|
||||
console.log("Регион успешно добавлен.");
|
||||
document.getElementById("region-id").value="";
|
||||
document.getElementById("region-name").value="";
|
||||
document.getElementById("region-active").checked=!1;
|
||||
loadRegions(regionsCurrentPage);
|
||||
toastr.success(e.message);
|
||||
$("#addRegionModal").modal("hide");
|
||||
}else{
|
||||
throw console.log("Ошибка: Статус ответа не 'success'."),new Error(e.message||"Неизвестная ошибка");
|
||||
}
|
||||
})
|
||||
.catch(e=>{
|
||||
console.error("Ошибка при добавлении региона:",e);
|
||||
toastr.error(e.message||"Ошибка при добавлении региона. Пожалуйста, попробуйте позже.");
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("add-system-form").addEventListener("submit",e=>{
|
||||
e.preventDefault();
|
||||
let systemId=document.getElementById("system-id").value;
|
||||
let systemNameLat=document.getElementById("system-name-lat").value;
|
||||
let systemNameCyr=document.getElementById("system-name-cyr").value;
|
||||
|
||||
if(!/^\d+$/.test(systemId)){
|
||||
toastr.error("ID системы должен содержать только числа.");
|
||||
return;
|
||||
}
|
||||
|
||||
safeFetch("/telezab/rest/api/systems",{
|
||||
method:"POST",
|
||||
headers:{"Content-Type":"application/json"},
|
||||
body:JSON.stringify({system_id:systemId,system_name:systemNameLat,name:systemNameCyr})
|
||||
})
|
||||
.then(e=>e.ok?e.json():e.json().then(e=>{
|
||||
throw new Error(e.message||"Ошибка добавления системы");
|
||||
}))
|
||||
.then(e=>{
|
||||
document.getElementById("system-id").value="";
|
||||
document.getElementById("system-name-lat").value="";
|
||||
document.getElementById("system-name-cyr").value="";
|
||||
loadSystems(systemsCurrentPage);
|
||||
toastr.success("Система успешно добавлена.");
|
||||
$("#addSystemModal").modal("hide");
|
||||
})
|
||||
.catch(e=>{
|
||||
console.error("Ошибка при добавлении системы:",e);
|
||||
toastr.error(e.message||"Ошибка при добавлении системы. Пожалуйста, попробуйте позже.");
|
||||
});
|
||||
});
|
||||
|
||||
// Инициализация при загрузке страницы
|
||||
document.addEventListener("DOMContentLoaded",()=>{
|
||||
let regionsTab=document.getElementById("regions-tab");
|
||||
let systemsTab=document.getElementById("systems-tab");
|
||||
|
||||
regionsTab.addEventListener("shown.bs.tab",()=>{
|
||||
loadRegions(currentPage);
|
||||
});
|
||||
|
||||
systemsTab.addEventListener("shown.bs.tab",()=>{
|
||||
loadSystems(systemsCurrentPage);
|
||||
});
|
||||
|
||||
regionsTab.classList.contains("active")&&loadRegions(currentPage);
|
||||
|
||||
// Очистка форм при закрытии модальных окон
|
||||
let modalsConfig=[
|
||||
{modalId:"addRegionModal",formId:"add-region-form"},
|
||||
{modalId:"addSystemModal",formId:"add-system-form"},
|
||||
{modalId:"editRegionNameModal",formId:"edit-region-name-form"},
|
||||
{modalId:"editSystemNameModal",formId:"edit-system-name-form"}
|
||||
];
|
||||
|
||||
modalsConfig.forEach(({modalId,formId})=>{
|
||||
let modal=document.getElementById(modalId);
|
||||
let form=document.getElementById(formId);
|
||||
if(modal&&form){
|
||||
modal.addEventListener("hidden.bs.modal",()=>{
|
||||
form.reset();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -1,21 +0,0 @@
|
||||
|
||||
window.safeFetch = function(url, options = {}) {
|
||||
return fetch(url, options).then(response => {
|
||||
if (response.status === 401) {
|
||||
window.location.href = `${window.location.origin}/telezab/login`;
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
return response;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
window.safeFetchJson = function(url, options = {}) {
|
||||
return fetch(url, options).then(response => {
|
||||
if (response.status === 401) {
|
||||
window.location.href = `${window.location.origin}/telezab/login`;
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
return response.json();
|
||||
});
|
||||
};
|
||||
@ -1,104 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
html {
|
||||
background-color: #56baed;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Poppins", sans-serif;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#formContent {
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
padding: 30px;
|
||||
width: 90%;
|
||||
max-width: 450px;
|
||||
box-shadow: 0 30px 60px 0 rgba(0,0,0,0.3);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#formFooter {
|
||||
background-color: #f6f6f6;
|
||||
border-top: 1px solid #dce8f1;
|
||||
padding: 25px;
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
||||
|
||||
input[type=text], input[type=password] {
|
||||
background-color: #f6f6f6;
|
||||
border: none;
|
||||
color: #0d0d0d;
|
||||
padding: 15px;
|
||||
font-size: 16px;
|
||||
margin: 10px;
|
||||
width: 95%;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
background-color: #56baed;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
text-transform: uppercase;
|
||||
font-size: 13px;
|
||||
box-shadow: 0 10px 30px 0 rgba(95,186,233,0.4);
|
||||
border-radius: 5px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
input[type=submit]:hover {
|
||||
background-color: #39ace7;
|
||||
}
|
||||
|
||||
.fadeIn {
|
||||
animation: fadeIn ease-in 1s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
#icon {
|
||||
width: 60%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="wrapper fadeInDown">
|
||||
<div id="formContent">
|
||||
<!-- Форма входа -->
|
||||
<form method="POST" action="{{ url_for('auth.login') }}">
|
||||
<input type="text" id="username" class="fadeIn second form-control text-center" name="username" placeholder="Имя пользователя" required>
|
||||
<input type="password" id="password" class="fadeIn third form-control text-center" name="password" placeholder="Пароль" required>
|
||||
<input type="submit" class="fadeIn fourth btn btn-primary w-100 mt-3" value="Войти">
|
||||
</form>
|
||||
<!-- Flash-сообщения -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="mt-3 alert alert-{{ category }} alert-dismissible fade show w-100" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,131 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Логи{% endblock %}
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/logs.css') }}">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2 class="mb-4">Журнал действий</h2>
|
||||
|
||||
<!-- Форма фильтрации -->
|
||||
<form method="get" class="mb-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md">
|
||||
<label for="action" class="form-label">Тип действия</label>
|
||||
<input type="text" class="form-control" id="action" name="action"
|
||||
value="{{ filters.action or '' }}">
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<label for="username" class="form-label">Пользователь</label>
|
||||
<input type="text" class="form-control" id="username" name="username"
|
||||
value="{{ filters.username or '' }}">
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<label for="timestamp_from" class="form-label">Дата с</label>
|
||||
<input type="date" class="form-control" id="timestamp_from" name="timestamp_from"
|
||||
value="{{ filters.timestamp_from or '' }}">
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<label for="timestamp_to" class="form-label">Дата по</label>
|
||||
<input type="date" class="form-control" id="timestamp_to" name="timestamp_to"
|
||||
value="{{ filters.timestamp_to or '' }}">
|
||||
</div>
|
||||
<div class="col-md-auto align-self-end">
|
||||
<button type="submit" class="btn btn-primary">Фильтровать</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Таблица логов -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>
|
||||
{% set new_order = 'asc' if filters.order != 'asc' else 'desc' %}
|
||||
<a href="{{ url_for('dashboard.logs_page', page=1, order=new_order,
|
||||
action=filters.action, username=filters.username,
|
||||
timestamp_from=filters.timestamp_from, timestamp_to=filters.timestamp_to) }}">
|
||||
Дата и время
|
||||
{% if filters.order == 'asc' %}
|
||||
▲
|
||||
{% elif filters.order == 'desc' %}
|
||||
▼
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th>Пользователь</th>
|
||||
<th>Тип действия</th>
|
||||
<th>Описание</th>
|
||||
<th>IP-адрес</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs.items %}
|
||||
<tr>
|
||||
<td>{{ log.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td>
|
||||
<td>{{ log.username }}</td>
|
||||
<td>{{ log.action }}</td>
|
||||
<td>{{ log.details }}</td>
|
||||
<td>{{ log.ipaddress }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center">Нет записей</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Пагинация -->
|
||||
<nav>
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if logs.has_prev %}
|
||||
<li class="page-item">
|
||||
<a class="page-link"
|
||||
href="{{ url_for('dashboard.logs_page', page=logs.prev_num, order=filters.order,
|
||||
action=filters.action, username=filters.username,
|
||||
timestamp_from=filters.timestamp_from, timestamp_to=filters.timestamp_to) }}">
|
||||
Назад
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">Назад</span></li>
|
||||
{% endif %}
|
||||
|
||||
{% for page_num in logs.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
||||
{% if page_num %}
|
||||
{% if page_num == logs.page %}
|
||||
<li class="page-item active"><span class="page-link">{{ page_num }}</span></li>
|
||||
{% else %}
|
||||
<li class="page-item">
|
||||
<a class="page-link"
|
||||
href="{{ url_for('dashboard.logs_page', page=page_num, order=filters.order,
|
||||
action=filters.action, username=filters.username,
|
||||
timestamp_from=filters.timestamp_from, timestamp_to=filters.timestamp_to) }}">
|
||||
{{ page_num }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if logs.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link"
|
||||
href="{{ url_for('dashboard.logs_page', page=logs.next_num, order=filters.order,
|
||||
action=filters.action, username=filters.username,
|
||||
timestamp_from=filters.timestamp_from, timestamp_to=filters.timestamp_to) }}">
|
||||
Вперёд
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">Вперёд</span></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,57 +0,0 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from aio_pika import connect_robust, exceptions as aio_exceptions
|
||||
from app import create_app, db
|
||||
from app.models import Users
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
rate_limit_semaphore = asyncio.Semaphore(25)
|
||||
|
||||
RABBITMQ_URL = "amqp://guest:guest@localhost/"
|
||||
RABBITMQ_QUEUE = "your_queue"
|
||||
|
||||
async def send_message(backend_bot, chat_id, message_text):
|
||||
telegram_id = "unknown"
|
||||
|
||||
try:
|
||||
async with rate_limit_semaphore:
|
||||
async def get_user():
|
||||
with app.app_context():
|
||||
user = Users.query.get(chat_id)
|
||||
return user.telegram_id if user else "unknown"
|
||||
|
||||
telegram_id = await asyncio.to_thread(get_user)
|
||||
await asyncio.to_thread(
|
||||
backend_bot.bot.send_message,
|
||||
chat_id,
|
||||
message_text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
logger.info(f"[RabbitMQ] Sent to {telegram_id} ({chat_id}): {message_text}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending message to {telegram_id} ({chat_id}): {e}")
|
||||
|
||||
async def consume_from_queue(backend_bot):
|
||||
while True:
|
||||
try:
|
||||
connection = await connect_robust(RABBITMQ_URL)
|
||||
async with connection:
|
||||
channel = await connection.channel()
|
||||
queue = await channel.declare_queue(RABBITMQ_QUEUE, durable=True)
|
||||
|
||||
async for message in queue:
|
||||
async with message.process():
|
||||
try:
|
||||
data = json.loads(message.body.decode('utf-8'))
|
||||
await send_message(backend_bot, data["chat_id"], data["message"])
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
logger.error(f"Error decoding message: {e}")
|
||||
|
||||
except aio_exceptions.AMQPError as e:
|
||||
logger.error(f"RabbitMQ AMQPError: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Unhandled error in consumer: {e}")
|
||||
finally:
|
||||
await asyncio.sleep(5)
|
||||
262
backend/api.py
Normal file
@ -0,0 +1,262 @@
|
||||
from flask import jsonify, request, Blueprint
|
||||
from flask_login import login_required
|
||||
|
||||
from frontend.dashboard import user_manager, event_manager, region_manager, system_manager
|
||||
from utilities.database import db
|
||||
from utilities.web_logger import WebLogger
|
||||
|
||||
bp_api = Blueprint('api', __name__, url_prefix='/telezab/rest/api')
|
||||
web_logger = WebLogger(db)
|
||||
|
||||
|
||||
@bp_api.route('/users', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def manage_users():
|
||||
if request.method == 'GET':
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
return jsonify(user_manager.get_users(page, per_page))
|
||||
elif request.method == 'POST':
|
||||
user_data = request.get_json()
|
||||
try:
|
||||
result, status_code = user_manager.add_user(user_data)
|
||||
if status_code == 201:
|
||||
web_logger.log_web_action(
|
||||
action='Добавление пользователя Telegram',
|
||||
details=f'Telegram ID: {user_data.get("chat_id")}, Username: {user_data.get("username")}'
|
||||
)
|
||||
return jsonify(result), status_code
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@bp_api.route('/users/<int:chat_id>', methods=['GET'])
|
||||
@login_required
|
||||
def get_user(chat_id):
|
||||
user = user_manager.get_user(chat_id)
|
||||
if not user:
|
||||
return jsonify({'error': 'Пользователь не найден'}), 404
|
||||
return jsonify(user)
|
||||
|
||||
|
||||
@bp_api.route('/users/<int:chat_id>/block', methods=['POST'])
|
||||
@login_required
|
||||
def block_user(chat_id):
|
||||
user_info = user_manager.get_user(chat_id)
|
||||
blocked = user_manager.toggle_block_user(chat_id)
|
||||
if blocked is not None:
|
||||
status = 'заблокирован' if blocked else 'разблокирован'
|
||||
web_logger.log_web_action(
|
||||
action=f'Блокировка/разблокировка пользователя Telegram',
|
||||
details=f'Telegram ID: {chat_id}, Username: {user_info.get("username") if user_info else "неизвестно"}, Статус: {status}'
|
||||
)
|
||||
return jsonify({'status': 'updated', 'new_status': blocked})
|
||||
else:
|
||||
return jsonify({'status': 'error', 'message': 'User not found'}), 404
|
||||
|
||||
|
||||
@bp_api.route('/users/<int:chat_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_user(chat_id):
|
||||
user_info = user_manager.get_user(chat_id)
|
||||
if user_manager.delete_user(chat_id):
|
||||
web_logger.log_web_action(
|
||||
action='Удаление пользователя Telegram',
|
||||
details=f'Telegram ID: {chat_id}, Username: {user_info.get("username") if user_info else "неизвестно"}'
|
||||
)
|
||||
return jsonify({'status': 'deleted'})
|
||||
else:
|
||||
return jsonify({'status': 'error', 'message': 'User not found'}), 404
|
||||
|
||||
|
||||
@bp_api.route('/users/<int:chat_id>/log', methods=['POST'])
|
||||
@login_required
|
||||
def log_user_action(chat_id):
|
||||
action = request.json.get('action')
|
||||
if action:
|
||||
event_manager.log_user_action(chat_id, action)
|
||||
return jsonify({'message': 'Действие сохранено'}), 200
|
||||
else:
|
||||
return jsonify({'error': 'Не указано действие'}), 400
|
||||
|
||||
@bp_api.route('/users/search', methods=['GET'])
|
||||
@login_required
|
||||
def search_users():
|
||||
telegram_id = request.args.get('telegram_id')
|
||||
email = request.args.get('email')
|
||||
users = user_manager.search_users(telegram_id, email)
|
||||
return jsonify(users)
|
||||
|
||||
@bp_api.route('/user_events/<int:chat_id>', methods=['GET'])
|
||||
@login_required
|
||||
def handle_user_events(chat_id):
|
||||
return event_manager.get_user_events(chat_id)
|
||||
|
||||
|
||||
|
||||
@bp_api.route('/regions', methods=['GET', 'POST', 'PUT', 'DELETE'])
|
||||
@login_required
|
||||
def manage_regions():
|
||||
if request.method == 'POST':
|
||||
region_data = request.get_json()
|
||||
result = region_manager.add_region(region_data)
|
||||
web_logger.log_web_action(
|
||||
action='Добавление региона',
|
||||
details=f'Название: {region_data.get("name")}, Номер: {region_data.get("number")}'
|
||||
)
|
||||
return jsonify(result)
|
||||
elif request.method == 'PUT':
|
||||
region_data = request.get_json()
|
||||
if 'active' in region_data:
|
||||
result = region_manager.update_region_status(region_data)
|
||||
status = 'активирован' if region_data.get('active') else 'деактивирован'
|
||||
web_logger.log_web_action(
|
||||
action='Изменение статуса региона',
|
||||
details=f'ID: {region_data.get("region_id")}, Статус: {status}'
|
||||
)
|
||||
return jsonify(result)
|
||||
elif 'name' in region_data:
|
||||
result = region_manager.update_region_name(region_data)
|
||||
web_logger.log_web_action(
|
||||
action='Изменение названия региона',
|
||||
details=f'ID: {region_data.get("region_id")}, Новое название: {region_data.get("name")}'
|
||||
)
|
||||
return jsonify(result)
|
||||
else:
|
||||
return jsonify({'status': 'error', 'message': 'Некорректный запрос'}), 400
|
||||
elif request.method == 'DELETE':
|
||||
region_id = request.args.get('region_id')
|
||||
region_info = region_manager.get_region(region_id)
|
||||
result = region_manager.delete_region(region_id)
|
||||
if result.get('status') == 'success':
|
||||
web_logger.log_web_action(
|
||||
action='Удаление региона',
|
||||
details=f'ID: {region_id}, Название: {region_info.get("region_name") if region_info else "неизвестно"}'
|
||||
)
|
||||
return jsonify(result)
|
||||
|
||||
@bp_api.route('/regions/<region_id>/subscribers', methods=['GET'])
|
||||
@login_required
|
||||
def get_region_subscribers(region_id):
|
||||
result, status_code = region_manager.get_region_subscribers(region_id)
|
||||
return jsonify(result), status_code
|
||||
|
||||
@bp_api.route('/systems', methods=['GET'])
|
||||
@login_required
|
||||
def get_systems():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 10, type=int)
|
||||
sort_field = request.args.get('sort_field', 'system_id')
|
||||
sort_order = request.args.get('sort_order', 'asc')
|
||||
|
||||
result = system_manager.get_systems(page, per_page, sort_field, sort_order)
|
||||
return jsonify(result)
|
||||
|
||||
@bp_api.route('/systems', methods=['POST', 'PUT', 'DELETE'])
|
||||
@login_required
|
||||
def manage_systems():
|
||||
if request.method == 'POST':
|
||||
data = request.get_json()
|
||||
result, status_code = system_manager.add_system(data)
|
||||
if status_code == 201:
|
||||
web_logger.log_web_action(
|
||||
action='Добавление системы',
|
||||
details=f'ID: {data.get("system_id")}, Название: {data.get("name")}'
|
||||
)
|
||||
return jsonify(result), status_code
|
||||
elif request.method == 'PUT':
|
||||
data = request.get_json()
|
||||
system_info_before = system_manager.get_system(data.get('system_id'))
|
||||
result, status_code = system_manager.update_system_name(data)
|
||||
if status_code == 200:
|
||||
web_logger.log_web_action(
|
||||
action='Изменение названия системы',
|
||||
details=f'ID: {data.get("system_id")}, Старое название: {system_info_before.get("name") if system_info_before else "неизвестно"}, Новое название: {data.get("name")}'
|
||||
)
|
||||
return jsonify(result), status_code
|
||||
elif request.method == 'DELETE':
|
||||
system_id = request.args.get('system_id')
|
||||
system_info = system_manager.get_system(system_id)
|
||||
result, status_code = system_manager.delete_system(system_id)
|
||||
if status_code == 200:
|
||||
web_logger.log_web_action(
|
||||
action='Удаление системы',
|
||||
details=f'ID: {system_id}, Название: {system_info.get("name") if system_info else "неизвестно"}'
|
||||
)
|
||||
return jsonify(result), status_code
|
||||
|
||||
@bp_api.route('/web_logs', methods=['GET'])
|
||||
@login_required
|
||||
def get_web_logs():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
ldap_user_id_filter = request.args.get('user_id', None, type=str)
|
||||
action_filter = request.args.get('action', None, type=str)
|
||||
|
||||
logs_data = web_logger.get_web_action_logs(page, per_page, ldap_user_id_filter, action_filter)
|
||||
return jsonify(logs_data)
|
||||
|
||||
#
|
||||
# @bp_api.route('/systems', methods=['POST'])
|
||||
# @login_required
|
||||
# def add_system():
|
||||
# data = request.get_json()
|
||||
# result, status_code = system_manager.add_system(data)
|
||||
# return jsonify(result), status_code
|
||||
#
|
||||
# @bp_api.route('/systems', methods=['PUT'])
|
||||
# @login_required
|
||||
# def update_system():
|
||||
# data = request.get_json()
|
||||
# result, status_code = system_manager.update_system_name(data)
|
||||
# return jsonify(result), status_code
|
||||
#
|
||||
# @bp_api.route('/systems', methods=['DELETE'])
|
||||
# @login_required
|
||||
# def delete_system():
|
||||
# system_id = request.args.get('system_id')
|
||||
# result, status_code = system_manager.delete_system(system_id)
|
||||
# return jsonify(result), status_code
|
||||
|
||||
@bp_api.route('/debug/log-level', methods=['POST'])
|
||||
@login_required
|
||||
def set_log_level():
|
||||
from telezab import log_manager
|
||||
try:
|
||||
data = request.get_json()
|
||||
component = data.get('component').lower()
|
||||
level = data.get('level').upper()
|
||||
success, message = log_manager.change_log_level(component, level)
|
||||
if success:
|
||||
return jsonify({'status': 'success', 'message': message}), 200
|
||||
else:
|
||||
return jsonify({'status': 'error', 'message': message}), 400
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@bp_api.route('/notifications', methods=['POST'])
|
||||
def notification():
|
||||
from utilities.notification_manager import NotificationManager
|
||||
from utilities.telegram_utilities import extract_region_number, format_message
|
||||
from backend_flask import app
|
||||
try:
|
||||
data = request.get_json()
|
||||
app.logger.info(f"Получены данные уведомления: {data}")
|
||||
region_id = extract_region_number(data.get("host"))
|
||||
if region_id is None:
|
||||
app.logger.error(f"Не удалось извлечь номер региона из host: {data.get('host')}")
|
||||
return jsonify({"status": "error", "message": "Invalid host format"}), 400
|
||||
app.logger.debug(f"Извлечён номер региона: {region_id}")
|
||||
|
||||
manager = NotificationManager(app.logger)
|
||||
subscribers = manager.get_subscribers(region_id, data['severity'])
|
||||
if manager.is_region_active(region_id):
|
||||
message = format_message(data)
|
||||
manager.send_notifications(subscribers, message)
|
||||
|
||||
return jsonify({"status": "success"}), 200
|
||||
|
||||
except Exception as e:
|
||||
app.logger.error(f"Ошибка при обработке уведомления: {e}")
|
||||
return jsonify({"status": "error", "message": "Внутренняя ошибка сервера"}), 500
|
||||
129
backend/auth.py
Normal file
@ -0,0 +1,129 @@
|
||||
import logging
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, session, current_app
|
||||
from flask_ldap3_login import LDAP3LoginManager, AuthenticationResponseStatus
|
||||
from flask_login import LoginManager, login_user, UserMixin, logout_user, current_user
|
||||
from datetime import timedelta
|
||||
|
||||
import config
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
bp_auth = Blueprint('auth', __name__, url_prefix='/telezab/')
|
||||
|
||||
login_manager = LoginManager()
|
||||
logging.getLogger('flask-login').setLevel(logging.DEBUG)
|
||||
logging.getLogger('flask_ldap3_login').setLevel(logging.DEBUG)
|
||||
logging.getLogger('ldap3').setLevel(logging.DEBUG)
|
||||
|
||||
class User(UserMixin):
|
||||
def __init__(self, user_id, user_name=None, user_surname=None, user_middle_name=None,display_name=None, email=None):
|
||||
self.id = str(user_id)
|
||||
self.user_name = user_name
|
||||
self.user_surname = user_surname
|
||||
self.user_middle_name = user_middle_name
|
||||
self.display_name = display_name
|
||||
self.email = email
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
logging.debug(f"load_user called for user_id: {user_id}")
|
||||
display_name = session.get('display_name') # Получаем display_name из сессии
|
||||
return User(user_id, display_name=display_name)
|
||||
|
||||
@bp_auth.record_once
|
||||
def on_load(state):
|
||||
login_manager.init_app(state.app)
|
||||
login_manager.login_view = 'auth.login'
|
||||
init_ldap(state.app)
|
||||
|
||||
|
||||
def init_ldap(app):
|
||||
app.config['LDAP_HOST'] = config.LDAP_HOST
|
||||
app.config['LDAP_PORT'] = config.LDAP_PORT
|
||||
app.config['LDAP_USE_SSL'] = config.LDAP_USE_SSL
|
||||
app.config['LDAP_BASE_DN'] = config.LDAP_BASE_DN
|
||||
app.config['LDAP_BIND_DIRECT_CREDENTIALS'] = False
|
||||
app.config['LDAP_BIND_USER_DN'] = config.LDAP_BIND_USER_DN
|
||||
app.config['LDAP_BIND_USER_PASSWORD'] = config.LDAP_USER_PASSWORD
|
||||
app.config['LDAP_USER_DN'] = config.LDAP_USER_DN
|
||||
app.config['LDAP_USER_PASSWORD'] = config.LDAP_USER_PASSWORD
|
||||
app.config['LDAP_USER_OBJECT_FILTER'] = config.LDAP_USER_OBJECT_FILTER
|
||||
app.config['LDAP_USER_LOGIN_ATTR'] = config.LDAP_USER_LOGIN_ATTR
|
||||
app.config['LDAP_USER_SEARCH_SCOPE'] = config.LDAP_USER_SEARCH_SCOPE
|
||||
app.config['LDAP_SCHEMA'] = config.LDAP_SCHEMA
|
||||
|
||||
ldap_manager = LDAP3LoginManager(app)
|
||||
app.extensions['ldap3_login'] = ldap_manager
|
||||
ldap_manager.init_app(app)
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1)
|
||||
|
||||
def get_attr(user_info, attr_name):
|
||||
try:
|
||||
value = user_info.get(attr_name)
|
||||
if isinstance(value, list) and value:
|
||||
return str(value[0])
|
||||
elif value:
|
||||
return str(value)
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting attribute {attr_name}: {e}")
|
||||
return None
|
||||
|
||||
@bp_auth.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if 'user_id' in session:
|
||||
return redirect(url_for('dashboard.dashboard'))
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
ldap_manager = current_app.extensions['ldap3_login']
|
||||
|
||||
try:
|
||||
ldap_response = ldap_manager.authenticate(username, password)
|
||||
logging.debug(f"ldap_response.status: {ldap_response.status}")
|
||||
|
||||
if ldap_response.status == AuthenticationResponseStatus.success:
|
||||
user_info = ldap_response.user_info
|
||||
logging.debug(f"user_info: {user_info}")
|
||||
|
||||
if not user_info:
|
||||
logging.error("LDAP authentication succeeded but no user info was returned.")
|
||||
flash("Failed to retrieve user details from LDAP.", "danger")
|
||||
return render_template("login.html")
|
||||
|
||||
sam_account_name = get_attr(user_info, "sAMAccountName")
|
||||
# display_name = get_attr(user_info, "displayName")
|
||||
email = get_attr(user_info, "mail")
|
||||
user_name = get_attr(user_info, "givenName")
|
||||
user_middle_name = get_attr(user_info, "middleName")
|
||||
user_surname = get_attr(user_info, "sn")
|
||||
display_name = f"{user_surname} {user_name} {user_middle_name}"
|
||||
user = User(user_id=sam_account_name,
|
||||
user_name=user_name,
|
||||
user_surname=user_surname,
|
||||
user_middle_name=user_middle_name,
|
||||
display_name=display_name,
|
||||
email=email
|
||||
)
|
||||
|
||||
session.permanent = True
|
||||
session['username'] = sam_account_name
|
||||
session['display_name'] = display_name # Сохраняем display_name в сессии
|
||||
login_user(user)
|
||||
logging.debug(f"current_user: {current_user.__dict__}")
|
||||
logging.info(f"User {user.id} logged in successfully.")
|
||||
# log_user_action(action='Успешная авторизация', details=f'Username: {username}') # Логируем успешную авторизацию
|
||||
flash("Logged in successfully!", "success")
|
||||
return redirect(url_for("dashboard.dashboard"))
|
||||
|
||||
elif ldap_response.status == AuthenticationResponseStatus.fail:
|
||||
flash('Invalid username or password.', 'danger')
|
||||
else:
|
||||
flash(f"LDAP Error: {ldap_response.status}", 'danger')
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error during login: {e}")
|
||||
flash("An unexpected error occurred. Please try again.", 'danger')
|
||||
|
||||
return render_template('login.html')
|
||||
103
backend_bot.py
@ -1,12 +1,10 @@
|
||||
import sqlite3
|
||||
import telebot
|
||||
|
||||
import telezab
|
||||
from app import app
|
||||
from backend_locks import bot
|
||||
from bot_database import is_whitelisted, format_regions_list, get_sorted_regions, log_user_event, \
|
||||
from backend_locks import db_lock, bot
|
||||
from bot_database import get_admins, is_whitelisted, format_regions_list, get_sorted_regions, log_user_event, \
|
||||
get_user_subscribed_regions
|
||||
from app.models import Regions, Subscriptions
|
||||
from app.extensions.db import db
|
||||
from config import DB_PATH
|
||||
from utilities.telegram_utilities import show_main_menu, show_settings_menu
|
||||
from handlers import handle_my_subscriptions_button, handle_active_regions_button, handle_notification_mode_button
|
||||
|
||||
@ -30,6 +28,7 @@ def handle_main_menu(message, chat_id, text):
|
||||
|
||||
def handle_settings_menu(message, chat_id, text):
|
||||
"""Обработка команд в меню настроек."""
|
||||
admins_list = get_admins()
|
||||
if text.lower() == 'подписаться':
|
||||
telezab.state.set_state(chat_id, "SUBSCRIBE")
|
||||
handle_subscribe_button(message)
|
||||
@ -84,40 +83,29 @@ def process_subscription_button(message, chat_id, username):
|
||||
reply_markup=markup)
|
||||
bot.register_next_step_handler_by_chat_id(chat_id, process_subscription_button, chat_id, username)
|
||||
return
|
||||
region_ids = [int(part.strip()) for part in message.text.split(',')]
|
||||
|
||||
with app.app_context():
|
||||
# Получаем список валидных ID регионов из базы
|
||||
valid_region_ids = [r.region_id for r in Regions.query.filter(Regions.active == True).all()]
|
||||
|
||||
region_ids = message.text.split(',')
|
||||
valid_region_ids = [region[0] for region in get_sorted_regions()]
|
||||
with db_lock:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
for region_id in region_ids:
|
||||
region_id = region_id.strip()
|
||||
if region_id not in valid_region_ids:
|
||||
invalid_regions.append(str(region_id))
|
||||
invalid_regions.append(region_id)
|
||||
continue
|
||||
|
||||
subscription = Subscriptions.query.filter_by(chat_id=chat_id, region_id=region_id).first()
|
||||
if subscription:
|
||||
if not subscription.active:
|
||||
subscription.active = True
|
||||
db.session.add(subscription)
|
||||
subbed_regions.append(str(region_id))
|
||||
else:
|
||||
# Уже подписан, можно тоже добавить для отчета
|
||||
subbed_regions.append(str(region_id))
|
||||
else:
|
||||
new_sub = Subscriptions(chat_id=chat_id, region_id=region_id, active=True)
|
||||
db.session.add(new_sub)
|
||||
subbed_regions.append(str(region_id))
|
||||
|
||||
db.session.commit()
|
||||
|
||||
if invalid_regions:
|
||||
bot.send_message(chat_id, f"Регион с ID {', '.join(invalid_regions)} не существует. Введите корректные номера или 'отмена'.")
|
||||
|
||||
if subbed_regions:
|
||||
bot.send_message(chat_id, f"Подписка на регионы: {', '.join(subbed_regions)} оформлена.")
|
||||
log_user_event(chat_id, username, f"Subscribed to regions: {', '.join(subbed_regions)}")
|
||||
|
||||
cursor.execute(
|
||||
'INSERT OR IGNORE INTO subscriptions (chat_id, region_id, username, active) VALUES (?, ?, ?, TRUE)',
|
||||
(chat_id, region_id, username))
|
||||
if cursor.rowcount == 0:
|
||||
cursor.execute('UPDATE subscriptions SET active = TRUE WHERE chat_id = ? AND region_id = ?',
|
||||
(chat_id, region_id))
|
||||
subbed_regions.append(region_id)
|
||||
conn.commit()
|
||||
if len(invalid_regions) > 0:
|
||||
bot.send_message(chat_id,
|
||||
f"Регион с ID {', '.join(invalid_regions)} не существует. Введите корректные номера или 'отмена'.")
|
||||
bot.send_message(chat_id, f"Подписка на регионы: {', '.join(subbed_regions)} оформлена.")
|
||||
log_user_event(chat_id, username, f"Subscribed to regions: {', '.join(subbed_regions)}")
|
||||
telezab.state.set_state(chat_id, "SETTINGS_MENU")
|
||||
show_settings_menu(chat_id)
|
||||
|
||||
@ -153,45 +141,34 @@ def handle_unsubscribe_button(message):
|
||||
def process_unsubscription_button(message, chat_id, username):
|
||||
unsubbed_regions = []
|
||||
invalid_regions = []
|
||||
|
||||
markup = telebot.types.InlineKeyboardMarkup()
|
||||
markup.add(telebot.types.InlineKeyboardButton(text="Отмена", callback_data="cancel_action"))
|
||||
|
||||
markup.add(telebot.types.InlineKeyboardButton(text="Отмена", callback_data=f"cancel_action"))
|
||||
if message.text.lower() == 'отмена':
|
||||
bot.send_message(chat_id, "Действие отменено.")
|
||||
telezab.state.set_state(chat_id, "SETTINGS_MENU")
|
||||
return show_settings_menu(chat_id)
|
||||
|
||||
# Проверка корректности формата ввода
|
||||
# Проверка, что введённая строка содержит только цифры и запятые
|
||||
if not all(part.strip().isdigit() for part in message.text.split(',')):
|
||||
bot.send_message(chat_id, "Некорректный формат. Введите номера регионов через запятую.", reply_markup=markup)
|
||||
bot.register_next_step_handler_by_chat_id(chat_id, process_unsubscription_button, chat_id, username)
|
||||
return
|
||||
|
||||
region_ids = [region_id.strip() for region_id in message.text.split(',')]
|
||||
|
||||
with app.app_context():
|
||||
valid_region_ids = [str(region[0]) for region in get_user_subscribed_regions(chat_id)] # get_user_subscribed_regions уже внутри app_context
|
||||
|
||||
region_ids = message.text.split(',')
|
||||
valid_region_ids = [region[0] for region in get_user_subscribed_regions(chat_id)]
|
||||
with db_lock:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
for region_id in region_ids:
|
||||
region_id = region_id.strip()
|
||||
if region_id not in valid_region_ids:
|
||||
invalid_regions.append(region_id)
|
||||
continue
|
||||
|
||||
subscription = db.session.query(Subscriptions).filter_by(
|
||||
chat_id=chat_id,
|
||||
region_id=int(region_id)
|
||||
).first()
|
||||
|
||||
if subscription:
|
||||
subscription.active = False
|
||||
unsubbed_regions.append(region_id)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
if invalid_regions:
|
||||
bot.send_message(chat_id, f"Регион(ы) с ID {', '.join(invalid_regions)} не найдены в ваших подписках.")
|
||||
|
||||
# Удаление подписки
|
||||
query = 'UPDATE subscriptions SET active = FALSE WHERE chat_id = ? AND region_id = ?'
|
||||
cursor.execute(query, (chat_id, region_id))
|
||||
unsubbed_regions.append(region_id)
|
||||
conn.commit()
|
||||
if len(invalid_regions) > 0:
|
||||
bot.send_message(chat_id, f"Регион с ID {', '.join(invalid_regions)} не найден в ваших подписках.")
|
||||
bot.send_message(chat_id, f"Отписка от регионов: {', '.join(unsubbed_regions)} выполнена.")
|
||||
log_user_event(chat_id, username, f"Unsubscribed from regions: {', '.join(unsubbed_regions)}")
|
||||
telezab.state.set_state(chat_id, "SETTINGS_MENU")
|
||||
|
||||
152
backend_flask.py
Normal file
@ -0,0 +1,152 @@
|
||||
import logging
|
||||
import sqlite3
|
||||
|
||||
from flask import Flask, request, jsonify, redirect, url_for
|
||||
from flask_login import LoginManager
|
||||
|
||||
import config
|
||||
from frontend.dashboard import bp_dashboard
|
||||
from backend.api import bp_api
|
||||
from backend.auth import bp_auth, User
|
||||
from backend_locks import db_lock
|
||||
from config import DB_PATH, TZ
|
||||
from utilities.database import db
|
||||
from utilities.telegram_utilities import extract_region_number, format_message
|
||||
|
||||
login_manager = LoginManager()
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__, static_url_path='/telezab/static', template_folder='templates')
|
||||
app.config['SECRET_KEY'] = config.SECRET_KEY # Замените на надежный секретный ключ
|
||||
app.config['SESSION_COOKIE_SECURE'] = config.SESSION_COOKIE_SECURE # Убедитесь, что установлено значение True
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = config.SESSION_COOKIE_HTTPONLY # Убедитесь, что установлено значение True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = config.SESSION_COOKIE_SAMESITE
|
||||
app.config['SESSION_REFRESH_EACH_REQUEST'] = False
|
||||
app.config['PERMANENT_SESSION_LIFETIME'] = config.PERMANENT_SESSION_LIFETIME
|
||||
app.config['SESSION_COOKIE_MAX_AGE'] = 3600
|
||||
app.config['TIMEZONE'] = TZ
|
||||
|
||||
@login_manager.unauthorized_handler
|
||||
def unauthorized():
|
||||
logging.debug("Unauthorized access detected")
|
||||
if request.path.startswith('/telezab/rest/api'):
|
||||
return jsonify({'error': 'Не авторизован'}), 401
|
||||
else:
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
app.register_blueprint(bp_dashboard)
|
||||
app.register_blueprint(bp_auth)
|
||||
app.register_blueprint(bp_api)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = config.SQLALCHEMY_DATABASE_URI
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
db.init_app(app)
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
login_manager.init_app(app) # Инициализация login_manager
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User(user_id)
|
||||
|
||||
return app
|
||||
|
||||
app = create_app()
|
||||
|
||||
|
||||
|
||||
@app.route('/telezab/webhook', methods=['POST'])
|
||||
def webhook():
|
||||
try:
|
||||
# Получаем данные и логируем
|
||||
data = request.get_json()
|
||||
app.logger.info(f"Получены данные: {data}")
|
||||
|
||||
# Работа с базой данных в блоке синхронизации
|
||||
with db_lock:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Проверяем количество записей в таблице событий
|
||||
cursor.execute('SELECT COUNT(*) FROM events')
|
||||
count = cursor.fetchone()[0]
|
||||
app.logger.debug(f"Текущее количество записей в таблице events: {count}")
|
||||
|
||||
# Если записей >= 200, удаляем самое старое событие
|
||||
if count >= 200:
|
||||
query = 'DELETE FROM events WHERE id = (SELECT MIN(id) FROM events)'
|
||||
app.logger.debug(f"Удаление старого события: {query}")
|
||||
cursor.execute(query)
|
||||
|
||||
# Извлечение номера региона из поля host
|
||||
region_id = extract_region_number(data.get("host"))
|
||||
if region_id is None:
|
||||
app.logger.error(f"Не удалось извлечь номер региона из host: {data.get('host')}")
|
||||
return jsonify({"status": "error", "message": "Invalid host format"}), 400
|
||||
app.logger.debug(f"Извлечён номер региона: {region_id}")
|
||||
|
||||
# Запрос подписчиков для отправки уведомления в зависимости от уровня критичности
|
||||
if data['severity'] == 'Disaster': # Авария
|
||||
query = 'SELECT chat_id, username FROM subscriptions WHERE region_id = ? AND active = TRUE'
|
||||
else: # Высокая критичность
|
||||
query = 'SELECT chat_id, username FROM subscriptions WHERE region_id = ? AND active = TRUE AND disaster_only = FALSE'
|
||||
|
||||
app.logger.debug(f"Выполнение запроса: {query} для region_id={region_id}")
|
||||
cursor.execute(query, (region_id,))
|
||||
results = cursor.fetchall()
|
||||
|
||||
app.logger.debug(f"Найдено подписчиков: {len(results)} для региона {region_id}")
|
||||
|
||||
# Проверка статуса региона (активен или нет)
|
||||
query = 'SELECT active FROM regions WHERE region_id = ?'
|
||||
cursor.execute(query, (region_id,))
|
||||
region_row = cursor.fetchone()
|
||||
|
||||
if region_row and region_row[0]: # Если регион активен
|
||||
app.logger.debug(f"Регион {region_id} активен. Начинаем рассылку сообщений.")
|
||||
message = format_message(data)
|
||||
undelivered = False
|
||||
|
||||
# Отправляем сообщения подписчикам
|
||||
for chat_id, username in results:
|
||||
formatted_message = message.replace('\n', ' ').replace('\r', '')
|
||||
|
||||
app.logger.info(
|
||||
f"Формирование сообщения для пользователя {username} (chat_id={chat_id}) [{formatted_message}]")
|
||||
try:
|
||||
from utilities.rabbitmq import send_to_queue
|
||||
send_to_queue({'chat_id': chat_id, 'username': username, 'message': message})
|
||||
app.logger.debug(f"Сообщение поставлено в очередь для {chat_id} (@{username})")
|
||||
except Exception as e:
|
||||
app.logger.error(f"Ошибка при отправке сообщения для {chat_id} (@{username}): {e}")
|
||||
undelivered = True
|
||||
|
||||
# Сохранение события, если были проблемы с доставкой
|
||||
if undelivered:
|
||||
query = 'INSERT OR IGNORE INTO events (hash, data, delivered) VALUES (?, ?, ?)'
|
||||
app.logger.debug(
|
||||
f"Сохранение события в базе данных: {query} (delivered={False})")
|
||||
cursor.execute(query, (str(data), False))
|
||||
|
||||
# Коммитим изменения в базе данных
|
||||
conn.commit()
|
||||
app.logger.debug("Изменения в базе данных успешно сохранены.")
|
||||
conn.close()
|
||||
|
||||
# Возвращаем успешный ответ
|
||||
return jsonify({"status": "success"}), 200
|
||||
|
||||
except sqlite3.OperationalError as e:
|
||||
app.logger.error(f"Ошибка операции с базой данных: {e}")
|
||||
return jsonify({"status": "error", "message": "Ошибка работы с базой данных"}), 500
|
||||
|
||||
except ValueError as e:
|
||||
app.logger.error(f"Ошибка значения: {e}")
|
||||
return jsonify({"status": "error", "message": "Некорректные данные"}), 400
|
||||
|
||||
except Exception as e:
|
||||
app.logger.error(f"Неожиданная ошибка: {e}")
|
||||
return jsonify({"status": "error", "message": "Внутренняя ошибка сервера"}), 500
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
import threading
|
||||
|
||||
import telebot
|
||||
|
||||
db_lock = threading.Lock()
|
||||
# bot_instance.py
|
||||
from config import TOKEN
|
||||
|
||||
bot = telebot.TeleBot(TOKEN)
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
@ -9,7 +8,6 @@ from pyzabbix import ZabbixAPI, ZabbixAPIException
|
||||
import backend_bot
|
||||
from config import ZABBIX_URL, ZABBIX_API_TOKEN
|
||||
from utilities.telegram_utilities import show_main_menu, escape_telegram_chars
|
||||
verify_ssl = os.getenv("ZAPPI_IGNORE_SSL_VERIFY", "True").lower() not in ("false", "0", "no")
|
||||
|
||||
zabbix_logger = logging.getLogger("pyzabbix")
|
||||
|
||||
@ -19,22 +17,25 @@ def get_triggers_for_group(chat_id, group_id):
|
||||
triggers = get_zabbix_triggers(group_id)
|
||||
if not triggers:
|
||||
backend_bot.bot.send_message(chat_id, "Нет активных событий.")
|
||||
zabbix_logger.debug(f"No active triggers found for group {group_id}.")
|
||||
show_main_menu(chat_id)
|
||||
else:
|
||||
send_triggers_to_user(triggers, chat_id)
|
||||
zabbix_logger.debug(f"Sent {len(triggers)} triggers to user {chat_id} for group {group_id}.")
|
||||
except ZabbixAPIException as e:
|
||||
zabbix_logger.error(f"Zabbix API error for group {group_id}: {e}")
|
||||
backend_bot.bot.send_message(chat_id, "Ошибка Zabbix API.")
|
||||
show_main_menu(chat_id)
|
||||
except Exception as e:
|
||||
zabbix_logger.error(f"Error getting triggers for group {group_id}: {e}")
|
||||
backend_bot.bot.send_message(chat_id, "Ошибка при получении событий.")
|
||||
show_main_menu(chat_id)
|
||||
|
||||
|
||||
def get_triggers_for_all_groups(chat_id, region_id):
|
||||
try:
|
||||
zapi = ZabbixAPI(ZABBIX_URL)
|
||||
zapi.login(api_token=ZABBIX_API_TOKEN)
|
||||
zapi.session.verify = verify_ssl
|
||||
|
||||
host_groups = zapi.hostgroup.get(output=["groupid", "name"], search={"name": region_id})
|
||||
filtered_groups = [group for group in host_groups if 'test' not in group['name'].lower()]
|
||||
@ -86,7 +87,7 @@ def get_zabbix_triggers(group_id):
|
||||
try:
|
||||
zapi = ZabbixAPI(ZABBIX_URL)
|
||||
zapi.login(api_token=ZABBIX_API_TOKEN)
|
||||
zapi.session.verify = verify_ssl
|
||||
|
||||
problems = zapi.problem.get(
|
||||
severities=[4, 5],
|
||||
suppressed=0,
|
||||
|
||||
129
bot_database.py
@ -1,12 +1,13 @@
|
||||
from datetime import datetime, timezone
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from threading import Lock
|
||||
|
||||
import telebot
|
||||
|
||||
from app import app
|
||||
from app.models import UserEvents, Regions, Subscriptions
|
||||
from app.models import Users
|
||||
from app.extensions.db import db
|
||||
from backend_flask import app
|
||||
from config import DB_PATH
|
||||
from models import UserEvents, Users
|
||||
from utilities.database import db
|
||||
|
||||
# Lock for database operations
|
||||
db_lock = Lock()
|
||||
@ -27,33 +28,105 @@ def is_whitelisted(chat_id):
|
||||
telebot.logger.error(f"Ошибка при проверке пользователя: {e}")
|
||||
return False, "Произошла ошибка при проверке доступа."
|
||||
|
||||
|
||||
def rundeck_add_to_whitelist(chat_id, username, user_email):
|
||||
with db_lock:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Проверка существования chat_id
|
||||
check_query = 'SELECT COUNT(*) FROM whitelist WHERE chat_id = ?'
|
||||
cursor.execute(check_query, (chat_id,))
|
||||
count = cursor.fetchone()[0]
|
||||
|
||||
if count > 0:
|
||||
conn.close()
|
||||
return False # Пользователь уже существует
|
||||
|
||||
# Вставка нового пользователя
|
||||
insert_query = 'INSERT INTO whitelist (chat_id, username, user_email) VALUES (?, ?, ?)'
|
||||
telebot.logger.info(
|
||||
f"Rundeck executing query: {insert_query} with chat_id={chat_id}, username={username}, email={user_email}")
|
||||
cursor.execute(insert_query, (chat_id, username, user_email))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True # Успешное добавление
|
||||
|
||||
|
||||
def remove_from_whitelist(chat_id):
|
||||
with db_lock:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
query = 'DELETE FROM whitelist WHERE chat_id = ?'
|
||||
telebot.logger.info(f"Executing query: {query} with chat_id={chat_id}")
|
||||
cursor.execute(query, (chat_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_admins():
|
||||
with db_lock:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT chat_id FROM admins')
|
||||
admins = cursor.fetchall()
|
||||
admins = [i[0] for i in admins]
|
||||
conn.close()
|
||||
return admins
|
||||
|
||||
|
||||
def get_sorted_regions():
|
||||
with app.app_context():
|
||||
regions = (
|
||||
db.session.query(Regions.region_id, Regions.region_name)
|
||||
.filter(Regions.active == True)
|
||||
.order_by(Regions.region_id.asc())
|
||||
.all()
|
||||
)
|
||||
return regions
|
||||
with db_lock:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT region_id, region_name FROM regions WHERE active = TRUE')
|
||||
regions = cursor.fetchall()
|
||||
conn.close()
|
||||
# Сортируем регионы по числовому значению region_id
|
||||
regions.sort(key=lambda x: int(x[0]))
|
||||
return regions
|
||||
|
||||
|
||||
def region_exists(region_id):
|
||||
with db_lock:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ? AND active = TRUE', (region_id,))
|
||||
count = cursor.fetchone()[0]
|
||||
conn.close()
|
||||
return count > 0
|
||||
|
||||
|
||||
def get_user_subscribed_regions(chat_id):
|
||||
with app.app_context(): # если вызывается вне контекста Flask
|
||||
results = (
|
||||
db.session.query(Regions.region_id, Regions.region_name)
|
||||
.join(Subscriptions, Subscriptions.region_id == Regions.region_id)
|
||||
.filter(
|
||||
Subscriptions.chat_id == chat_id,
|
||||
Subscriptions.active == True,
|
||||
Subscriptions.skip == False
|
||||
)
|
||||
.order_by(Regions.region_id.asc())
|
||||
.all()
|
||||
)
|
||||
with db_lock:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT regions.region_id, regions.region_name
|
||||
FROM subscriptions
|
||||
JOIN regions ON subscriptions.region_id = regions.region_id
|
||||
WHERE subscriptions.chat_id = ? AND subscriptions.active = TRUE AND subscriptions.skip = FALSE
|
||||
ORDER BY regions.region_id
|
||||
''', (chat_id,))
|
||||
regions = cursor.fetchall()
|
||||
conn.close()
|
||||
# Сортируем регионы по числовому значению region_id
|
||||
regions.sort(key=lambda x: int(x[0]))
|
||||
return regions
|
||||
|
||||
# results — это список кортежей (region_id, region_name)
|
||||
return results
|
||||
|
||||
def is_subscribed(chat_id, region_id):
|
||||
with db_lock:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT COUNT(*)
|
||||
FROM subscriptions
|
||||
WHERE chat_id = ? AND region_id = ? AND active = TRUE AND skip = FALSE
|
||||
''', (chat_id, region_id))
|
||||
count = cursor.fetchone()[0]
|
||||
conn.close()
|
||||
return count > 0
|
||||
|
||||
|
||||
def format_regions_list(regions):
|
||||
@ -64,7 +137,7 @@ def log_user_event(chat_id, username, action):
|
||||
"""Логирует действие пользователя с использованием ORM."""
|
||||
try:
|
||||
with app.app_context(): # Создаем контекст приложения
|
||||
timestamp = datetime.now(timezone.utc) # Оставляем объект datetime для БД
|
||||
timestamp = datetime.now(datetime.UTC) # Оставляем объект datetime для БД
|
||||
formatted_time = timestamp.strftime('%Y-%m-%d %H:%M:%S') # Форматируем для логов
|
||||
|
||||
event = UserEvents(
|
||||
|
||||
57
frontend/dashboard.py
Normal file
@ -0,0 +1,57 @@
|
||||
import logging
|
||||
|
||||
from flask import Blueprint, render_template, redirect, url_for, session
|
||||
from sqlalchemy import create_engine
|
||||
from config import DB_PATH
|
||||
from utilities.database import db
|
||||
from utilities.events_manager import EventManager
|
||||
from utilities.region_manager import RegionManager
|
||||
from utilities.system_manager import SystemManager
|
||||
from utilities.users_manager import UserManager
|
||||
from models import Users
|
||||
from flask_login import logout_user, login_required
|
||||
|
||||
# Создаём Blueprint
|
||||
bp_dashboard = Blueprint('dashboard', __name__, url_prefix='/telezab/')
|
||||
|
||||
db_engine = create_engine(f'sqlite:///{DB_PATH}')
|
||||
|
||||
|
||||
region_manager = RegionManager()
|
||||
user_manager = UserManager(db.session)
|
||||
event_manager = EventManager(db)
|
||||
system_manager = SystemManager()
|
||||
|
||||
|
||||
# Роуты для отображения страниц
|
||||
@bp_dashboard.route('/')
|
||||
@login_required
|
||||
def dashboard():
|
||||
return render_template('index.html')
|
||||
|
||||
@bp_dashboard.route('/users')
|
||||
@login_required
|
||||
def users_page():
|
||||
users = Users.query.all()
|
||||
return render_template('users.html', user=users)
|
||||
|
||||
@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')
|
||||
|
||||
@bp_dashboard.route('/health')
|
||||
def healthcheck():
|
||||
pass
|
||||
|
||||
@bp_dashboard.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
session.clear()
|
||||
return redirect(url_for('auth.login'))
|
||||
19
frontend/models.py
Normal 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
@ -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
@ -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)
|
||||
9
frontend/routes/regions.py
Normal 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
@ -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)
|
||||
63
models.py
Normal file
@ -0,0 +1,63 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import PrimaryKeyConstraint, ForeignKey, Integer, String, DateTime
|
||||
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
||||
|
||||
from utilities.database import db # Импортируем db из backend_flask.py
|
||||
|
||||
class Users(db.Model):
|
||||
chat_id = db.Column(db.Integer, primary_key=True)
|
||||
telegram_id = db.Column(db.String(80), unique=True, nullable=False)
|
||||
user_email = db.Column(db.String(255), unique=True, nullable=False)
|
||||
is_blocked = db.Column(db.Boolean, default=False)
|
||||
subscriptions = relationship("Subscriptions", backref="user", cascade="all, delete-orphan") # Добавлено cascade
|
||||
|
||||
class Regions(db.Model):
|
||||
region_id = db.Column(db.Integer, primary_key=True)
|
||||
region_name = db.Column(db.String(255), nullable=False)
|
||||
active = db.Column(db.Boolean, default=True)
|
||||
|
||||
class Subscriptions(db.Model):
|
||||
region_id = db.Column(db.Integer, nullable=False)
|
||||
active = db.Column(db.Boolean, default=True)
|
||||
skip = db.Column(db.Boolean, default=False)
|
||||
disaster_only = db.Column(db.Boolean, default=False)
|
||||
chat_id = db.Column(db.Integer, ForeignKey('users.chat_id', ondelete='CASCADE'), nullable=False) #Добавляем внешний ключ с ondelete
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint('chat_id', 'region_id'),
|
||||
)
|
||||
|
||||
class UILogs(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
chat_id = db.Column(db.Integer, nullable=False)
|
||||
actions = db.Column(db.String(500), nullable=False)
|
||||
timestamp = db.Column(db.DateTime, default=db.func.current_timestamp())
|
||||
|
||||
class UserEvents(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
chat_id = db.Column(db.Integer, nullable=False)
|
||||
telegram_id = 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())
|
||||
|
||||
class Systems(db.Model):
|
||||
__tablename__ = 'systems'
|
||||
system_id = db.Column(db.String(255), primary_key=True)
|
||||
system_name = db.Column(db.String(255), nullable=False)
|
||||
name = db.Column(db.String(255), nullable=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<System {self.system_id}: {self.system_name} {self.name}>'
|
||||
|
||||
class WebActionLog(db.Model):
|
||||
__tablename__ = 'web_action_logs'
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
ldap_user_id: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
username: Mapped[str | None] = mapped_column(String(255))
|
||||
timestamp: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
action: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
details: Mapped[str | None] = mapped_column(String(1024))
|
||||
|
||||
def __repr__(self):
|
||||
return f"<WebActionLog(ldap_user_id='{self.ldap_user_id}', username='{self.username}', action='{self.action}', timestamp='{self.timestamp}')>"
|
||||
@ -1,11 +1,50 @@
|
||||
telebot~=0.0.5
|
||||
pyTelegramBotAPI~=4.27.0
|
||||
pyzabbix~=1.3.1
|
||||
SQLAlchemy~=2.0.40
|
||||
Flask~=3.1.0
|
||||
Flask-Login~=0.6.3
|
||||
Werkzeug~=3.1.3
|
||||
aio-pika~=9.5.5
|
||||
pika~=1.3.2
|
||||
pytz~=2025.2
|
||||
concurrent-log-handler~=0.9.26
|
||||
aio-pika==9.5.4
|
||||
aiohappyeyeballs==2.4.6
|
||||
aiohttp==3.11.12
|
||||
aiormq==6.8.1
|
||||
aiosignal==1.3.2
|
||||
alembic==1.15.1
|
||||
attrs==25.1.0
|
||||
blinker==1.9.0
|
||||
cachelib==0.13.0
|
||||
certifi==2025.1.31
|
||||
charset-normalizer==3.4.1
|
||||
click==8.1.8
|
||||
colorama==0.4.6
|
||||
exceptiongroup==1.2.2
|
||||
Flask==3.1.0
|
||||
flask-ldap3-login==1.0.2
|
||||
Flask-Login==0.6.3
|
||||
Flask-Session==0.8.0
|
||||
Flask-SQLAlchemy==3.1.1
|
||||
Flask-WTF==1.2.2
|
||||
frozenlist==1.5.0
|
||||
greenlet==3.1.1
|
||||
gunicorn==23.0.0
|
||||
idna==3.10
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.5
|
||||
ldap3==2.9.1
|
||||
Mako==1.3.9
|
||||
MarkupSafe==3.0.2
|
||||
msgspec==0.19.0
|
||||
multidict==6.1.0
|
||||
packaging==24.2
|
||||
pamqp==3.3.0
|
||||
pika==1.3.2
|
||||
pika-stubs==0.1.3
|
||||
propcache==0.2.1
|
||||
pyasn1==0.6.1
|
||||
pyTelegramBotAPI==4.26.0
|
||||
python-dotenv==1.0.1
|
||||
pytz==2025.1
|
||||
pyzabbix==1.3.1
|
||||
requests==2.32.3
|
||||
schedule==1.2.2
|
||||
SQLAlchemy==2.0.38
|
||||
telebot==0.0.5
|
||||
typing_extensions==4.12.2
|
||||
urllib3==2.3.0
|
||||
Werkzeug==3.1.3
|
||||
WTForms==3.2.1
|
||||
yarl==1.18.3
|
||||
|
||||
|
Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 476 B |
|
Before Width: | Height: | Size: 507 B After Width: | Height: | Size: 507 B |
|
Before Width: | Height: | Size: 514 B After Width: | Height: | Size: 514 B |
|
Before Width: | Height: | Size: 579 B After Width: | Height: | Size: 579 B |
|
Before Width: | Height: | Size: 250 B After Width: | Height: | Size: 250 B |
|
Before Width: | Height: | Size: 279 B After Width: | Height: | Size: 279 B |
|
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 286 B |
|
Before Width: | Height: | Size: 366 B After Width: | Height: | Size: 366 B |
|
Before Width: | Height: | Size: 854 B After Width: | Height: | Size: 854 B |
|
Before Width: | Height: | Size: 457 B After Width: | Height: | Size: 457 B |
|
Before Width: | Height: | Size: 477 B After Width: | Height: | Size: 477 B |
|
Before Width: | Height: | Size: 484 B After Width: | Height: | Size: 484 B |
|
Before Width: | Height: | Size: 564 B After Width: | Height: | Size: 564 B |
|
Before Width: | Height: | Size: 607 B After Width: | Height: | Size: 607 B |
|
Before Width: | Height: | Size: 642 B After Width: | Height: | Size: 642 B |
|
Before Width: | Height: | Size: 634 B After Width: | Height: | Size: 634 B |
|
Before Width: | Height: | Size: 714 B After Width: | Height: | Size: 714 B |
|
Before Width: | Height: | Size: 359 B After Width: | Height: | Size: 359 B |
|
Before Width: | Height: | Size: 421 B After Width: | Height: | Size: 421 B |
|
Before Width: | Height: | Size: 428 B After Width: | Height: | Size: 428 B |
|
Before Width: | Height: | Size: 493 B After Width: | Height: | Size: 493 B |
|
Before Width: | Height: | Size: 495 B After Width: | Height: | Size: 495 B |
|
Before Width: | Height: | Size: 514 B After Width: | Height: | Size: 514 B |