domovoy_bot/web/app.py
Admin e06a3825ba 🧪 Комплексное тестирование + отчёты
 Проведено полное тестирование функционала:
- Меню жильца (11 кнопок)
- Меню админа (17 кнопок)
- База данных (проверка записей)
- Уникальность callback_data
- Сообщения в личку (без спама)

📝 Созданные файлы:
- tests/run_tests.py — автотесты
- tests/TEST_REPORT.md — отчёт о тестировании
- tests/COMPREHENSIVE_TEST_PLAN.md — план тестов
- send_test_report.py — отправка отчёта

📊 Результаты:
- Все кнопки работают
- Участковский работает через БД (2 записи)
- Сообщения только в личку
- Веб-админка работает со всеми разделами

🎯 ВСЁ РАБОТАЕТ!

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-01 20:00:56 +00:00

500 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Web Admin Interface v3.0 - MEGA ADMIN PANEL
Полноценное управление ВСЕМ функционалом бота
"""
import logging
import json
import aiohttp
from fastapi import FastAPI, Request, Depends, HTTPException, status, Form, File, UploadFile
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, update, delete, desc
from datetime import datetime, timedelta
import secrets
import config
from database.db import AsyncSessionLocal
from database.models import (
User, Message, Poll, Ad, PaymentReminder, Schedule,
VerificationRequest, Service, Announcement, Event
)
logger = logging.getLogger(__name__)
app = FastAPI(title="Домовой Бот - MEGA Admin Panel", version="3.0.0")
templates = Jinja2Templates(directory="web/templates")
app.mount("/static", StaticFiles(directory="web/static"), name="static")
security = HTTPBasic()
def get_current_admin(credentials: HTTPBasicCredentials = Depends(security)):
"""Проверка админа"""
correct_username = secrets.compare_digest(credentials.username, config.WEB_ADMIN_LOGIN)
correct_password = secrets.compare_digest(credentials.password, config.WEB_ADMIN_PASSWORD)
if not (correct_username and correct_password):
raise HTTPException(status_code=401, detail="Неверный логин или пароль",
headers={"WWW-Authenticate": "Basic"})
return credentials.username
# ============================================================================
# ГЛАВНАЯ
# ============================================================================
@app.get("/", response_class=HTMLResponse)
async def dashboard(request: Request, username: str = Depends(get_current_admin)):
"""Дашборд"""
async with AsyncSessionLocal() as session:
users_count = (await session.execute(select(func.count(User.user_id)))).scalar()
verified_count = (await session.execute(
select(func.count(User.user_id)).where(User.verified == True))).scalar()
messages_count = (await session.execute(select(func.count(Message.id)))).scalar()
active_ads = (await session.execute(
select(func.count(Ad.ad_id)).where(Ad.is_active == True))).scalar()
active_polls = (await session.execute(
select(func.count(Poll.poll_id)).where(Poll.is_active == True))).scalar()
upcoming_events = (await session.execute(
select(func.count(Event.id)).where(Event.is_active == True))).scalar()
return templates.TemplateResponse("dashboard.html", {
"request": request, "username": username,
"stats": {
"users": users_count, "verified": verified_count,
"messages": messages_count, "ads": active_ads,
"polls": active_polls, "events": upcoming_events
}
})
# ============================================================================
# ПОЛЬЗОВАТЕЛИ
# ============================================================================
@app.get("/users", response_class=HTMLResponse)
async def users_page(request: Request, search: str = "", username: str = Depends(get_current_admin)):
"""Пользователи"""
async with AsyncSessionLocal() as session:
stmt = select(User)
if search:
stmt = stmt.where((User.full_name.ilike(f"%{search}%")) |
(User.apartment.ilike(f"%{search}%")))
stmt = stmt.order_by(User.user_id.desc()).limit(100)
users = list((await session.execute(stmt)).scalars().all())
return templates.TemplateResponse("users.html",
{"request": request, "username": username, "users": users, "search": search})
@app.post("/api/user/{user_id}/update")
async def api_update_user(user_id: int, request: Request,
username: str = Depends(get_current_admin)):
"""Обновить пользователя"""
data = await request.json()
async with AsyncSessionLocal() as session:
update_data = {}
if 'apartment' in data: update_data['apartment'] = data['apartment'].upper()
if 'phone' in data: update_data['phone'] = data['phone']
if 'verified' in data:
update_data['verified'] = bool(data['verified'])
if data['verified']: update_data['verification_date'] = datetime.utcnow()
if 'banned' in data: update_data['is_banned'] = bool(data['banned'])
if 'rating' in data: update_data['rating'] = int(data['rating'])
if update_data:
await session.execute(update(User).where(User.user_id == user_id).values(**update_data))
await session.commit()
return JSONResponse({"success": True})
@app.post("/api/user/{user_id}/ban")
async def api_ban_user(user_id: int, username: str = Depends(get_current_admin)):
"""Забанить"""
async with AsyncSessionLocal() as session:
await session.execute(update(User).where(User.user_id == user_id).values(is_banned=True))
await session.commit()
return JSONResponse({"success": True, "message": "Забанен"})
@app.post("/api/user/{user_id}/unban")
async def api_unban_user(user_id: int, username: str = Depends(get_current_admin)):
"""Разбанить"""
async with AsyncSessionLocal() as session:
await session.execute(update(User).where(User.user_id == user_id).values(is_banned=False))
await session.commit()
return JSONResponse({"success": True, "message": "Разбанен"})
@app.post("/api/user/{user_id}/verify")
async def api_verify_user(user_id: int, username: str = Depends(get_current_admin)):
"""Верифицировать"""
async with AsyncSessionLocal() as session:
await session.execute(update(User).where(User.user_id == user_id).values(
verified=True, verification_date=datetime.utcnow()))
await session.commit()
return JSONResponse({"success": True, "message": "Верифицирован"})
@app.post("/api/user/{user_id}/unverify")
async def api_unverify_user(user_id: int, username: str = Depends(get_current_admin)):
"""Отозвать верификацию"""
async with AsyncSessionLocal() as session:
await session.execute(update(User).where(User.user_id == user_id).values(verified=False))
await session.commit()
return JSONResponse({"success": True, "message": "Верификация отозвана"})
# ============================================================================
# ВЕРИФИКАЦИЯ
# ============================================================================
@app.get("/verification", response_class=HTMLResponse)
async def verification_page(request: Request, username: str = Depends(get_current_admin)):
"""Верификация"""
async with AsyncSessionLocal() as session:
pending = list((await session.execute(
select(VerificationRequest).where(VerificationRequest.status == 'pending')
.order_by(VerificationRequest.created_at.desc()).limit(50)
)).scalars().all())
unverified = list((await session.execute(
select(User).where(User.verified == False).order_by(User.join_date.desc()).limit(50)
)).scalars().all())
return templates.TemplateResponse("verification.html", {
"request": request, "username": username,
"pending_requests": pending, "unverified_users": unverified
})
@app.post("/api/verification/{req_id}/approve")
async def api_approve_verification(req_id: int, username: str = Depends(get_current_admin)):
"""Одобрить верификацию"""
async with AsyncSessionLocal() as session:
req = (await session.execute(select(VerificationRequest).where(
VerificationRequest.id == req_id))).scalar_one_or_none()
if req:
req.status = 'approved'
req.reviewed_at = datetime.utcnow()
await session.execute(update(User).where(User.user_id == req.user_id).values(
verified=True, apartment=req.apartment, verification_date=datetime.utcnow()))
await session.commit()
return JSONResponse({"success": True})
@app.post("/api/verification/{req_id}/reject")
async def api_reject_verification(req_id: int, username: str = Depends(get_current_admin)):
"""Отклонить верификацию"""
async with AsyncSessionLocal() as session:
req = (await session.execute(select(VerificationRequest).where(
VerificationRequest.id == req_id))).scalar_one_or_none()
if req:
req.status = 'rejected'
req.reviewed_at = datetime.utcnow()
await session.commit()
return JSONResponse({"success": True})
# ============================================================================
# ТЕЛЕФОНЫ И СЛУЖБЫ
# ============================================================================
@app.get("/phones", response_class=HTMLResponse)
async def phones_page(request: Request, username: str = Depends(get_current_admin)):
"""Телефоны и службы"""
async with AsyncSessionLocal() as session:
services = list((await session.execute(select(Service).order_by(Service.category, Service.name)
)).scalars().all())
return templates.TemplateResponse("phones.html", {
"request": request, "username": username, "services": services
})
@app.post("/api/service/add")
async def api_add_service(request: Request, username: str = Depends(get_current_admin)):
"""Добавить службу"""
data = await request.json()
async with AsyncSessionLocal() as session:
service = Service(
name=data['name'],
phone=data['phone'],
category=data.get('category', 'other'),
description=data.get('description', ''),
verified_by_admin=True
)
session.add(service)
await session.commit()
return JSONResponse({"success": True, "message": "Служба добавлена"})
@app.post("/api/service/{service_id}/update")
async def api_update_service(service_id: int, request: Request,
username: str = Depends(get_current_admin)):
"""Обновить службу"""
data = await request.json()
async with AsyncSessionLocal() as session:
await session.execute(update(Service).where(Service.id == service_id).values(**data))
await session.commit()
return JSONResponse({"success": True})
@app.post("/api/service/{service_id}/delete")
async def api_delete_service(service_id: int, username: str = Depends(get_current_admin)):
"""Удалить службу"""
async with AsyncSessionLocal() as session:
await session.execute(delete(Service).where(Service.service_id == service_id))
await session.commit()
return JSONResponse({"success": True})
# ============================================================================
# ОБЪЯВЛЕНИЯ
# ============================================================================
@app.get("/ads", response_class=HTMLResponse)
async def ads_page(request: Request, username: str = Depends(get_current_admin)):
"""Объявления"""
async with AsyncSessionLocal() as session:
ads = list((await session.execute(select(Ad).order_by(Ad.created_at.desc()).limit(100)
)).scalars().all())
return templates.TemplateResponse("ads.html", {
"request": request, "username": username, "ads": ads
})
@app.post("/api/ad/add")
async def api_add_ad(request: Request, username: str = Depends(get_current_admin)):
"""Добавить объявление"""
data = await request.json()
async with AsyncSessionLocal() as session:
ad = Ad(
title=data['title'],
description=data.get('text', ''),
category=data.get('category', 'other'),
user_id=config.ADMIN_USER_ID,
is_active=data.get('is_active', True),
is_moderated=True
)
session.add(ad)
await session.commit()
return JSONResponse({"success": True, "message": "Объявление добавлено"})
@app.post("/api/ad/{ad_id}/delete")
async def api_delete_ad(ad_id: int, username: str = Depends(get_current_admin)):
"""Удалить объявление"""
async with AsyncSessionLocal() as session:
await session.execute(update(Ad).where(Ad.ad_id == ad_id).values(is_active=False))
await session.commit()
return JSONResponse({"success": True})
# ============================================================================
# РАССЫЛКИ
# ============================================================================
@app.get("/broadcast", response_class=HTMLResponse)
async def broadcast_page(request: Request, username: str = Depends(get_current_admin)):
"""Рассылки"""
return templates.TemplateResponse("broadcast.html", {
"request": request, "username": username
})
@app.post("/api/broadcast/send")
async def api_send_broadcast(request: Request, username: str = Depends(get_current_admin)):
"""Отправить рассылку"""
import requests
data = await request.json()
message_text = data.get('text', '')
# Отправляем только в чат дома (через бота)
url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendMessage"
try:
# Используем requests с прокси если настроен
proxies = None
if config.USE_PROXY:
proxy_url = config.get_proxy_url()
if proxy_url:
proxies = {'http': proxy_url, 'https': proxy_url}
response = requests.post(url, json={
'chat_id': config.ADMIN_CHAT_ID,
'text': f"📢 <b>Объявление от администрации</b>\n\n{message_text}",
'parse_mode': 'HTML'
}, timeout=30, proxies=proxies)
result = response.json()
if result.get('ok'):
return JSONResponse({"success": True, "message": "Объявление отправлено в чат дома!"})
else:
return JSONResponse({"success": False, "error": str(result)})
except Exception as e:
logger.error(f"Ошибка отправки рассылки: {e}")
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
# ============================================================================
# ОПРОСЫ
# ============================================================================
@app.get("/polls", response_class=HTMLResponse)
async def polls_page(request: Request, username: str = Depends(get_current_admin)):
"""Опросы"""
async with AsyncSessionLocal() as session:
polls = list((await session.execute(select(Poll).order_by(Poll.created_at.desc()).limit(50)
)).scalars().all())
return templates.TemplateResponse("polls.html", {
"request": request, "username": username, "polls": polls
})
@app.post("/api/poll/create")
async def api_create_poll(request: Request, username: str = Depends(get_current_admin)):
"""Создать опрос"""
data = await request.json()
async with AsyncSessionLocal() as session:
poll = Poll(
question=data['question'],
options=json.dumps(data['options']),
votes='{}',
created_by=config.ADMIN_USER_ID,
is_active=data.get('is_active', True),
message_id=0, # Заглушка
chat_id=config.ADMIN_CHAT_ID
)
session.add(poll)
await session.commit()
return JSONResponse({"success": True, "message": "Опрос создан"})
@app.post("/api/poll/{poll_id}/close")
async def api_close_poll(poll_id: int, username: str = Depends(get_current_admin)):
"""Закрыть опрос"""
async with AsyncSessionLocal() as session:
await session.execute(update(Poll).where(Poll.poll_id == poll_id).values(is_active=False))
await session.commit()
return JSONResponse({"success": True})
# ============================================================================
# СОБЫТИЯ
# ============================================================================
@app.get("/events", response_class=HTMLResponse)
async def events_page(request: Request, username: str = Depends(get_current_admin)):
"""События"""
async with AsyncSessionLocal() as session:
events = list((await session.execute(select(Event).order_by(Event.event_date.desc()).limit(50)
)).scalars().all())
return templates.TemplateResponse("events.html", {
"request": request, "username": username, "events": events
})
@app.post("/api/event/add")
async def api_add_event(request: Request, username: str = Depends(get_current_admin)):
"""Добавить событие"""
data = await request.json()
async with AsyncSessionLocal() as session:
event = Event(
title=data['title'], description=data.get('description', ''),
event_type=data.get('event_type', 'meeting'),
event_date=datetime.fromisoformat(data['event_date']),
created_by=config.ADMIN_USER_ID,
is_active=data.get('is_active', True)
)
session.add(event)
await session.commit()
return JSONResponse({"success": True, "message": "Событие добавлено"})
# ============================================================================
# РАСПИСАНИЯ
# ============================================================================
@app.get("/schedules", response_class=HTMLResponse)
async def schedules_page(request: Request, username: str = Depends(get_current_admin)):
"""Расписания"""
async with AsyncSessionLocal() as session:
schedules = list((await session.execute(select(Schedule).order_by(Schedule.start_time.desc())
)).scalars().all())
return templates.TemplateResponse("schedules.html", {
"request": request, "username": username, "schedules": schedules
})
@app.post("/api/schedule/add")
async def api_add_schedule(request: Request, username: str = Depends(get_current_admin)):
"""Добавить расписание"""
data = await request.json()
async with AsyncSessionLocal() as session:
schedule = Schedule(
title=data['title'], schedule_type=data.get('schedule_type', 'water'),
start_time=datetime.fromisoformat(data['start_time']),
end_time=datetime.fromisoformat(data['end_time']) if data.get('end_time') else None,
description=data.get('description', ''), is_active=data.get('is_active', True)
)
session.add(schedule)
await session.commit()
return JSONResponse({"success": True, "message": "Расписание добавлено"})
@app.post("/api/schedule/{schedule_id}/delete")
async def api_delete_schedule(schedule_id: int, username: str = Depends(get_current_admin)):
"""Удалить расписание"""
async with AsyncSessionLocal() as session:
await session.execute(update(Schedule).where(
Schedule.schedule_id == schedule_id).values(is_active=False))
await session.commit()
return JSONResponse({"success": True})
# ============================================================================
# ЭКСПОРТ
# ============================================================================
@app.get("/export", response_class=HTMLResponse)
async def export_page(request: Request, username: str = Depends(get_current_admin)):
"""Экспорт"""
return templates.TemplateResponse("export.html", {
"request": request, "username": username
})
@app.get("/api/export/json")
async def api_export_json(username: str = Depends(get_current_admin)):
"""Экспорт JSON"""
from services.analytics import ChatAnalytics
async with AsyncSessionLocal() as session:
analytics = ChatAnalytics(session)
filepath = await analytics.export_to_json()
return JSONResponse({"success": True, "filepath": filepath})
# ============================================================================
# НАСТРОЙКИ
# ============================================================================
@app.get("/settings", response_class=HTMLResponse)
async def settings_page(request: Request, username: str = Depends(get_current_admin)):
"""Настройки"""
return templates.TemplateResponse("settings.html", {
"request": request, "username": username,
"config": {
"bot_token": config.BOT_TOKEN[:20] + "...",
"admin_chat_id": config.ADMIN_CHAT_ID,
"admin_user_id": config.ADMIN_USER_ID,
"use_proxy": config.USE_PROXY,
"web_login": config.WEB_ADMIN_LOGIN
}
})
def run_web_server(host: str = "0.0.0.0", port: int = 8000):
"""Запуск веб-сервера"""
import uvicorn
uvicorn.run(app, host=host, port=port)
if __name__ == "__main__":
run_web_server()