Compare commits

..

2 Commits

Author SHA1 Message Date
52e31864b3 feat: Develop web interface
- Implemented the initial version of the web interface.
refactor: Begin Telegram bot refactoring
- Started restructuring the bot’s code for better maintainability.
chore: Migrate to Flask project structure
- Reorganized the application to follow Flask's project structure.
cleanup: Extensive code cleanup
- Removed redundant code and improved readability.

Signed-off-by: UdoChudo <stream@udochudo.ru>
2025-06-10 14:39:11 +05:00
acf4436fc4 Optimize requirements.txt file 2025-05-05 18:03:09 +05:00
2200 changed files with 3214 additions and 2687 deletions

82
app/__init__.py Normal file
View File

@ -0,0 +1,82 @@
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()

0
app/bot/__init__.py Normal file
View File

17
app/bot/config.py Normal file
View File

@ -0,0 +1,17 @@
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

15
app/bot/handlers/help.py Normal file
View File

@ -0,0 +1,15 @@
# 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

@ -0,0 +1,13 @@
# 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

@ -0,0 +1,21 @@
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

@ -0,0 +1,25 @@
# 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())

41
app/bot/handlers/start.py Normal file
View File

@ -0,0 +1,41 @@
# 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

View File

@ -0,0 +1,11 @@
# 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

@ -0,0 +1,9 @@
# 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

View File

@ -0,0 +1,49 @@
# 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} прошёл проверку")

0
app/bot/states.py Normal file
View File

24
app/bot/telezab_bot.py Normal file
View File

@ -0,0 +1,24 @@
# 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

View File

@ -0,0 +1,190 @@
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

@ -0,0 +1,10 @@
# 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)

3
app/extensions/db.py Normal file
View File

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

8
app/models/__init__.py Normal file
View File

@ -0,0 +1,8 @@
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

19
app/models/auditlog.py Normal file
View File

@ -0,0 +1,19 @@
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}')>"

6
app/models/regions.py Normal file
View File

@ -0,0 +1,6 @@
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)

10
app/models/state.py Normal file
View File

@ -0,0 +1,10 @@
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

@ -0,0 +1,13 @@
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'),
)

10
app/models/systems.py Normal file
View File

@ -0,0 +1,10 @@
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}>'

17
app/models/user.py Normal file
View File

@ -0,0 +1,17 @@
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

9
app/models/userevents.py Normal file
View File

@ -0,0 +1,9 @@
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())

10
app/models/users.py Normal file
View File

@ -0,0 +1,10 @@
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

8
app/routes/__init__.py Normal file
View File

@ -0,0 +1,8 @@
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

@ -0,0 +1,14 @@
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

@ -0,0 +1,12 @@
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

42
app/routes/api/regions.py Normal file
View File

@ -0,0 +1,42 @@
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)

31
app/routes/api/systems.py Normal file
View File

@ -0,0 +1,31 @@
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)

68
app/routes/api/users.py Normal file
View File

@ -0,0 +1,68 @@
# 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

59
app/routes/auth.py Normal file
View File

@ -0,0 +1,59 @@
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'))

92
app/routes/dashboard.py Normal file
View File

@ -0,0 +1,92 @@
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

@ -0,0 +1,53 @@
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

@ -0,0 +1,72 @@
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

@ -0,0 +1,28 @@
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

@ -0,0 +1,86 @@
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

@ -0,0 +1,299 @@
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

@ -0,0 +1,171 @@
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

@ -0,0 +1,37 @@
# 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

@ -0,0 +1,232 @@
# 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

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

View File

Before

Width:  |  Height:  |  Size: 521 B

After

Width:  |  Height:  |  Size: 521 B

View File

Before

Width:  |  Height:  |  Size: 601 B

After

Width:  |  Height:  |  Size: 601 B

View File

Before

Width:  |  Height:  |  Size: 617 B

After

Width:  |  Height:  |  Size: 617 B

View File

Before

Width:  |  Height:  |  Size: 640 B

After

Width:  |  Height:  |  Size: 640 B

View File

Before

Width:  |  Height:  |  Size: 662 B

After

Width:  |  Height:  |  Size: 662 B

View File

Before

Width:  |  Height:  |  Size: 727 B

After

Width:  |  Height:  |  Size: 727 B

View File

Before

Width:  |  Height:  |  Size: 251 B

After

Width:  |  Height:  |  Size: 251 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: 686 B

After

Width:  |  Height:  |  Size: 686 B

View File

Before

Width:  |  Height:  |  Size: 717 B

After

Width:  |  Height:  |  Size: 717 B

View File

Before

Width:  |  Height:  |  Size: 737 B

After

Width:  |  Height:  |  Size: 737 B

View File

Before

Width:  |  Height:  |  Size: 804 B

After

Width:  |  Height:  |  Size: 804 B

View File

Before

Width:  |  Height:  |  Size: 574 B

After

Width:  |  Height:  |  Size: 574 B

View File

Before

Width:  |  Height:  |  Size: 597 B

After

Width:  |  Height:  |  Size: 597 B

View File

Before

Width:  |  Height:  |  Size: 620 B

After

Width:  |  Height:  |  Size: 620 B

View File

Before

Width:  |  Height:  |  Size: 684 B

After

Width:  |  Height:  |  Size: 684 B

View File

Before

Width:  |  Height:  |  Size: 366 B

After

Width:  |  Height:  |  Size: 366 B

View File

Before

Width:  |  Height:  |  Size: 687 B

After

Width:  |  Height:  |  Size: 687 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 492 B

After

Width:  |  Height:  |  Size: 492 B

View File

Before

Width:  |  Height:  |  Size: 840 B

After

Width:  |  Height:  |  Size: 840 B

View File

Before

Width:  |  Height:  |  Size: 615 B

After

Width:  |  Height:  |  Size: 615 B

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