Compare commits
2 Commits
b94e8d4724
...
52e31864b3
| Author | SHA1 | Date | |
|---|---|---|---|
| 52e31864b3 | |||
| acf4436fc4 |
82
app/__init__.py
Normal 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
17
app/bot/config.py
Normal 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}/"
|
||||
0
app/bot/handlers/__init__.py
Normal file
15
app/bot/handlers/help.py
Normal 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")
|
||||
13
app/bot/handlers/main_menu.py
Normal 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()
|
||||
)
|
||||
21
app/bot/handlers/registration.py
Normal 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")
|
||||
25
app/bot/handlers/settings.py
Normal 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
@ -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, "Произошла ошибка. Попробуйте позже.")
|
||||
0
app/bot/keyboards/__init__.py
Normal file
11
app/bot/keyboards/main_menu.py
Normal 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
|
||||
9
app/bot/keyboards/settings_menu.py
Normal 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
|
||||
0
app/bot/middlewares/__init__.py
Normal file
49
app/bot/middlewares/user_access.py
Normal 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
24
app/bot/telezab_bot.py
Normal 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()
|
||||
0
app/extensions/__init__.py
Normal file
190
app/extensions/audit_logger.py
Normal 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)
|
||||
10
app/extensions/auth_ext.py
Normal 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
@ -0,0 +1,3 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db: SQLAlchemy = SQLAlchemy()
|
||||
8
app/models/__init__.py
Normal 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
@ -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
@ -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
@ -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}>"
|
||||
13
app/models/subscriptions.py
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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)
|
||||
14
app/routes/api/__init__.py
Normal 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')
|
||||
12
app/routes/api/notifications.py
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
|
||||
|
||||
53
app/services/auth_service.py
Normal 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"),
|
||||
}
|
||||
72
app/services/bot/bot_database.py
Normal 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}")
|
||||
28
app/services/notifications_service.py
Normal 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
|
||||
86
app/services/rabbitmq_service.py
Normal 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)
|
||||
299
app/services/regions_service.py
Normal 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
|
||||
171
app/services/systems_service.py
Normal 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
|
||||
37
app/services/users_event_service.py
Normal 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
|
||||
232
app/services/users_service.py
Normal 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
|
||||
|
Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 476 B |
|
Before Width: | Height: | Size: 507 B After Width: | Height: | Size: 507 B |
|
Before Width: | Height: | Size: 514 B After Width: | Height: | Size: 514 B |
|
Before Width: | Height: | Size: 579 B After Width: | Height: | Size: 579 B |
|
Before Width: | Height: | Size: 250 B After Width: | Height: | Size: 250 B |
|
Before Width: | Height: | Size: 279 B After Width: | Height: | Size: 279 B |
|
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 286 B |
|
Before Width: | Height: | Size: 366 B After Width: | Height: | Size: 366 B |
|
Before Width: | Height: | Size: 854 B After Width: | Height: | Size: 854 B |
|
Before Width: | Height: | Size: 457 B After Width: | Height: | Size: 457 B |
|
Before Width: | Height: | Size: 477 B After Width: | Height: | Size: 477 B |
|
Before Width: | Height: | Size: 484 B After Width: | Height: | Size: 484 B |
|
Before Width: | Height: | Size: 564 B After Width: | Height: | Size: 564 B |
|
Before Width: | Height: | Size: 607 B After Width: | Height: | Size: 607 B |
|
Before Width: | Height: | Size: 642 B After Width: | Height: | Size: 642 B |
|
Before Width: | Height: | Size: 634 B After Width: | Height: | Size: 634 B |
|
Before Width: | Height: | Size: 714 B After Width: | Height: | Size: 714 B |
|
Before Width: | Height: | Size: 359 B After Width: | Height: | Size: 359 B |
|
Before Width: | Height: | Size: 421 B After Width: | Height: | Size: 421 B |
|
Before Width: | Height: | Size: 428 B After Width: | Height: | Size: 428 B |
|
Before Width: | Height: | Size: 493 B After Width: | Height: | Size: 493 B |
|
Before Width: | Height: | Size: 495 B After Width: | Height: | Size: 495 B |
|
Before Width: | Height: | Size: 514 B After Width: | Height: | Size: 514 B |
|
Before Width: | Height: | Size: 521 B After Width: | Height: | Size: 521 B |
|
Before Width: | Height: | Size: 601 B After Width: | Height: | Size: 601 B |
|
Before Width: | Height: | Size: 617 B After Width: | Height: | Size: 617 B |
|
Before Width: | Height: | Size: 640 B After Width: | Height: | Size: 640 B |
|
Before Width: | Height: | Size: 662 B After Width: | Height: | Size: 662 B |
|
Before Width: | Height: | Size: 727 B After Width: | Height: | Size: 727 B |
|
Before Width: | Height: | Size: 251 B After Width: | Height: | Size: 251 B |
|
Before Width: | Height: | Size: 279 B After Width: | Height: | Size: 279 B |
|
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 286 B |
|
Before Width: | Height: | Size: 366 B After Width: | Height: | Size: 366 B |
|
Before Width: | Height: | Size: 686 B After Width: | Height: | Size: 686 B |
|
Before Width: | Height: | Size: 717 B After Width: | Height: | Size: 717 B |
|
Before Width: | Height: | Size: 737 B After Width: | Height: | Size: 737 B |
|
Before Width: | Height: | Size: 804 B After Width: | Height: | Size: 804 B |
|
Before Width: | Height: | Size: 574 B After Width: | Height: | Size: 574 B |
|
Before Width: | Height: | Size: 597 B After Width: | Height: | Size: 597 B |
|
Before Width: | Height: | Size: 620 B After Width: | Height: | Size: 620 B |
|
Before Width: | Height: | Size: 684 B After Width: | Height: | Size: 684 B |
|
Before Width: | Height: | Size: 366 B After Width: | Height: | Size: 366 B |
|
Before Width: | Height: | Size: 687 B After Width: | Height: | Size: 687 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 492 B After Width: | Height: | Size: 492 B |
|
Before Width: | Height: | Size: 840 B After Width: | Height: | Size: 840 B |
|
Before Width: | Height: | Size: 615 B After Width: | Height: | Size: 615 B |