Compare commits

..

No commits in common. "52e31864b30fea16c57e5553d2677c001d29df81" and "b94e8d472459f52cdb615ad4901839b89656242c" have entirely different histories.

2200 changed files with 2687 additions and 3214 deletions

View File

@ -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()

View File

View File

@ -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}/"

View File

@ -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")

View File

@ -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()
)

View File

@ -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")

View File

@ -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())

View File

@ -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, "Произошла ошибка. Попробуйте позже.")

View File

@ -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

View File

@ -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

View File

@ -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} прошёл проверку")

View File

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -1,3 +0,0 @@
from flask_sqlalchemy import SQLAlchemy
db: SQLAlchemy = SQLAlchemy()

View File

@ -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

View File

@ -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}')>"

View File

@ -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)

View File

@ -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}>"

View File

@ -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'),
)

View File

@ -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}>'

View File

@ -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

View File

@ -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())

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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'))

View File

@ -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

View File

@ -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"),
}

View File

@ -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}")

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -1,4 +0,0 @@
/* Курсор "рука" для иконки календаря (для Chrome и поддерживаемых браузеров) */
input[type="date"]::-webkit-calendar-picker-indicator {
cursor: pointer;
}

View File

@ -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;
}

View File

@ -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&&currentFetchController.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&&currentFetchController.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}')">&laquo;</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}')">&raquo;</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();
});
}
});
});

View File

@ -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();
});
};

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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
View 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
View 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')

View File

@ -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
View 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

View File

@ -1,4 +1,9 @@
import threading
import telebot
db_lock = threading.Lock()
# bot_instance.py
from config import TOKEN
bot = telebot.TeleBot(TOKEN)

View File

@ -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,

View File

@ -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
View 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
View File

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

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

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

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

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

View File

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

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

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

63
models.py Normal file
View 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}')>"

View File

@ -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

View File

Before

Width:  |  Height:  |  Size: 476 B

After

Width:  |  Height:  |  Size: 476 B

View File

Before

Width:  |  Height:  |  Size: 507 B

After

Width:  |  Height:  |  Size: 507 B

View File

Before

Width:  |  Height:  |  Size: 514 B

After

Width:  |  Height:  |  Size: 514 B

View File

Before

Width:  |  Height:  |  Size: 579 B

After

Width:  |  Height:  |  Size: 579 B

View File

Before

Width:  |  Height:  |  Size: 250 B

After

Width:  |  Height:  |  Size: 250 B

View File

Before

Width:  |  Height:  |  Size: 279 B

After

Width:  |  Height:  |  Size: 279 B

View File

Before

Width:  |  Height:  |  Size: 286 B

After

Width:  |  Height:  |  Size: 286 B

View File

Before

Width:  |  Height:  |  Size: 366 B

After

Width:  |  Height:  |  Size: 366 B

View File

Before

Width:  |  Height:  |  Size: 854 B

After

Width:  |  Height:  |  Size: 854 B

View File

Before

Width:  |  Height:  |  Size: 457 B

After

Width:  |  Height:  |  Size: 457 B

View File

Before

Width:  |  Height:  |  Size: 477 B

After

Width:  |  Height:  |  Size: 477 B

View File

Before

Width:  |  Height:  |  Size: 484 B

After

Width:  |  Height:  |  Size: 484 B

View File

Before

Width:  |  Height:  |  Size: 564 B

After

Width:  |  Height:  |  Size: 564 B

View File

Before

Width:  |  Height:  |  Size: 607 B

After

Width:  |  Height:  |  Size: 607 B

View File

Before

Width:  |  Height:  |  Size: 642 B

After

Width:  |  Height:  |  Size: 642 B

View File

Before

Width:  |  Height:  |  Size: 634 B

After

Width:  |  Height:  |  Size: 634 B

View File

Before

Width:  |  Height:  |  Size: 714 B

After

Width:  |  Height:  |  Size: 714 B

View File

Before

Width:  |  Height:  |  Size: 359 B

After

Width:  |  Height:  |  Size: 359 B

View File

Before

Width:  |  Height:  |  Size: 421 B

After

Width:  |  Height:  |  Size: 421 B

View File

Before

Width:  |  Height:  |  Size: 428 B

After

Width:  |  Height:  |  Size: 428 B

View File

Before

Width:  |  Height:  |  Size: 493 B

After

Width:  |  Height:  |  Size: 493 B

View File

Before

Width:  |  Height:  |  Size: 495 B

After

Width:  |  Height:  |  Size: 495 B

View File

Before

Width:  |  Height:  |  Size: 514 B

After

Width:  |  Height:  |  Size: 514 B

Some files were not shown because too many files have changed in this diff Show More