Files
dodep-simulator/admin.py
T

802 lines
27 KiB
Python

from fastapi import APIRouter, Request, Depends, Form, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from sqlalchemy import desc, func
from typing import Optional
import json
from datetime import datetime
from pathlib import Path
from database import get_db, User, InventoryItem, CaseOpening, Contract
from auth import get_current_admin
from rpu import get_user_rpu, calculate_rpu_level, get_rpu_description
admin_router = APIRouter(prefix="/admin", tags=["admin"])
templates = Jinja2Templates(directory="templates")
# Загрузка конфигов
CASES_FILE = Path("cases.json")
MAINPAGE_FILE = Path("mainpage.json")
def load_cases_config():
if CASES_FILE.exists():
with open(CASES_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return []
def save_cases_config(cases):
with open(CASES_FILE, 'w', encoding='utf-8') as f:
json.dump(cases, f, ensure_ascii=False, indent=2)
def load_mainpage_config():
if MAINPAGE_FILE.exists():
with open(MAINPAGE_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return []
def save_mainpage_config(config):
with open(MAINPAGE_FILE, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
def get_rpu_level_for_user(user_id: int) -> float:
"""Хелпер для шаблона"""
db = next(get_db())
rpu = get_user_rpu(db, user_id)
return calculate_rpu_level(rpu)
# ========== СТРАНИЦЫ ==========
@admin_router.get("/", response_class=HTMLResponse)
async def admin_dashboard(
request: Request,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
"""Главная страница админки"""
# Основная статистика
total_users = db.query(User).count()
total_items = db.query(InventoryItem).count()
total_openings = db.query(CaseOpening).count()
total_contracts = db.query(Contract).count()
total_balance = db.query(func.sum(User.balance)).scalar() or 0
# Статистика РПУ
from rpu import UserRPU
all_rpu = db.query(UserRPU).all()
rpu_count = len(all_rpu)
avg_rpu = 50.0
lucky_count = 0
normal_count = 0
unlucky_count = 0
auto_adjust_count = 0
total_spent = 0.0
if rpu_count > 0:
for r in all_rpu:
level = calculate_rpu_level(r)
if level > 60:
lucky_count += 1
elif level < 40:
unlucky_count += 1
else:
normal_count += 1
if r.auto_adjust:
auto_adjust_count += 1
total_spent += r.total_spent
avg_rpu = sum(calculate_rpu_level(r) for r in all_rpu) / rpu_count
recent_users = db.query(User).order_by(desc(User.created_at)).limit(10).all()
return templates.TemplateResponse("admin/dashboard.html", {
"request": request,
"user": admin,
"total_users": total_users,
"total_items": total_items,
"total_openings": total_openings,
"total_contracts": total_contracts,
"total_balance": total_balance,
"recent_users": recent_users,
"rpu_count": rpu_count,
"avg_rpu": round(avg_rpu, 1),
"lucky_count": lucky_count,
"normal_count": normal_count,
"unlucky_count": unlucky_count,
"auto_adjust_count": auto_adjust_count,
"total_spent": total_spent,
"get_rpu_level": get_rpu_level_for_user
})
@admin_router.get("/users", response_class=HTMLResponse)
async def admin_users(
request: Request,
search: Optional[str] = None,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
"""Список пользователей"""
query = db.query(User)
if search:
query = query.filter(User.username.contains(search))
users = query.order_by(desc(User.created_at)).limit(100).all()
# Добавляем уровень РПУ для каждого пользователя
users_with_rpu = []
for u in users:
rpu = get_user_rpu(db, u.id)
users_with_rpu.append({
"id": u.id,
"username": u.username,
"balance": u.balance,
"is_admin": u.is_admin,
"created_at": u.created_at,
"inventory_items": u.inventory_items,
"case_openings": u.case_openings,
"rpu_level": round(calculate_rpu_level(rpu))
})
return templates.TemplateResponse("admin/users.html", {
"request": request,
"user": admin,
"users": users_with_rpu,
"search": search
})
@admin_router.get("/user/{user_id}", response_class=HTMLResponse)
async def admin_user_detail(
user_id: int,
request: Request,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
raise HTTPException(404, "Пользователь не найден")
total_items = db.query(InventoryItem).filter(InventoryItem.user_id == user_id).count()
total_openings = db.query(CaseOpening).filter(CaseOpening.user_id == user_id).count()
total_contracts = db.query(Contract).filter(Contract.user_id == user_id).count()
inventory = db.query(InventoryItem).filter(
InventoryItem.user_id == user_id
).order_by(desc(InventoryItem.obtained_at)).limit(50).all()
return templates.TemplateResponse("admin/user_detail.html", {
"request": request,
"user": admin,
"target_user": target_user,
"total_items": total_items,
"total_openings": total_openings,
"total_contracts": total_contracts,
"inventory": inventory
})
@admin_router.get("/cases", response_class=HTMLResponse)
async def admin_cases(
request: Request,
admin: User = Depends(get_current_admin)
):
from frontend import ALL_ITEMS
cases = load_cases_config()
mainpage = load_mainpage_config()
for case in cases:
if not isinstance(case.get('items'), list):
case['items'] = []
all_items = []
for i, item in enumerate(ALL_ITEMS[:500]):
all_items.append({
"id": item.get("_id", i),
"name": item.get("market_hash_name", "Unknown"),
"rarity": item.get("rarity", "Unknown"),
"image_url": item.get("image_url", "")
})
return templates.TemplateResponse("admin/cases.html", {
"request": request,
"user": admin,
"cases": cases,
"mainpage": mainpage,
"all_items": all_items,
"cases_config": {c['name']: c for c in cases}
})
# ========== API ПОЛЬЗОВАТЕЛИ ==========
@admin_router.post("/api/user/{user_id}/balance")
async def admin_set_balance(
user_id: int,
amount: float = Form(...),
operation: str = Form(...),
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
return JSONResponse(status_code=404, content={"error": "Пользователь не найден"})
old_balance = target_user.balance
if operation == "add":
target_user.balance += amount
message = f"Добавлено {amount:.2f}"
elif operation == "remove":
target_user.balance = max(0, target_user.balance - amount)
message = f"Снято {amount:.2f}"
else:
target_user.balance = amount
message = f"Установлен баланс {amount:.2f}"
db.commit()
return JSONResponse({
"success": True,
"old_balance": old_balance,
"new_balance": target_user.balance,
"message": message
})
@admin_router.post("/api/user/{user_id}/give-item")
async def admin_give_item(
user_id: int,
item_id: int = Form(...),
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
from frontend import ALL_ITEMS, generate_item_float
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
return JSONResponse(status_code=404, content={"error": "Пользователь не найден"})
item_data = None
for idx, item in enumerate(ALL_ITEMS):
if item.get("_id", idx) == item_id:
item_data = item
break
if not item_data:
return JSONResponse(status_code=404, content={"error": "Предмет не найден"})
item_float = generate_item_float(item_data.get("rarity", "Unknown"))
inventory_item = InventoryItem(
user_id=user_id,
item_id=item_id,
market_hash_name=item_data.get("market_hash_name", "Unknown"),
rarity=item_data.get("rarity", "Unknown"),
wear=item_data.get("wear", "Unknown"),
float_value=item_float,
type=item_data.get("type", "Normal"),
obtained_from="admin"
)
db.add(inventory_item)
db.commit()
return JSONResponse({
"success": True,
"message": f"Предмет '{item_data.get('market_hash_name')}' выдан пользователю {target_user.username}"
})
@admin_router.post("/api/user/{user_id}/toggle-ban")
async def admin_toggle_ban(
user_id: int,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
return JSONResponse(status_code=404, content={"error": "Пользователь не найден"})
target_user.is_banned = not target_user.is_banned
db.commit()
return JSONResponse({
"success": True,
"is_banned": target_user.is_banned,
"message": f"Пользователь {'забанен' if target_user.is_banned else 'разбанен'}"
})
@admin_router.post("/api/user/{user_id}/toggle-admin")
async def admin_toggle_admin(
user_id: int,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
return JSONResponse(status_code=404, content={"error": "Пользователь не найден"})
if target_user.id == admin.id:
return JSONResponse(status_code=400, content={"error": "Нельзя изменить свой статус"})
target_user.is_admin = not target_user.is_admin
db.commit()
return JSONResponse({
"success": True,
"is_admin": target_user.is_admin,
"message": f"Статус администратора {'включен' if target_user.is_admin else 'выключен'}"
})
@admin_router.post("/api/user/{user_id}/delete-item/{inventory_id}")
async def admin_delete_item(
user_id: int,
inventory_id: int,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
item = db.query(InventoryItem).filter(
InventoryItem.id == inventory_id,
InventoryItem.user_id == user_id
).first()
if not item:
return JSONResponse(status_code=404, content={"error": "Предмет не найден"})
db.delete(item)
db.commit()
return JSONResponse({"success": True, "message": "Предмет удален"})
# ========== API РПУ ==========
@admin_router.get("/api/user/{user_id}/rpu")
async def admin_get_user_rpu(
user_id: int,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
return JSONResponse(status_code=404, content={"error": "Пользователь не найден"})
rpu = get_user_rpu(db, user_id)
return {
"user_id": user_id,
"username": target_user.username,
"multipliers": {
"consumer": rpu.consumer_multiplier,
"industrial": rpu.industrial_multiplier,
"mil_spec": rpu.mil_spec_multiplier,
"restricted": rpu.restricted_multiplier,
"classified": rpu.classified_multiplier,
"covert": rpu.covert_multiplier,
"rare_special": rpu.rare_special_multiplier,
},
"luck_multiplier": rpu.luck_multiplier,
"auto_adjust": rpu.auto_adjust,
"rpu_level": calculate_rpu_level(rpu),
"stats": {
"total_spent": rpu.total_spent,
"total_opened": rpu.total_opened
}
}
@admin_router.post("/api/user/{user_id}/rpu/update")
async def admin_update_user_rpu(
user_id: int,
consumer: float = Form(1.0),
industrial: float = Form(1.0),
mil_spec: float = Form(1.0),
restricted: float = Form(1.0),
classified: float = Form(1.0),
covert: float = Form(1.0),
rare_special: float = Form(1.0),
luck: float = Form(1.0),
auto_adjust: bool = Form(False),
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
return JSONResponse(status_code=404, content={"error": "Пользователь не найден"})
rpu = get_user_rpu(db, user_id)
rpu.consumer_multiplier = max(0.1, min(3.0, consumer))
rpu.industrial_multiplier = max(0.1, min(3.0, industrial))
rpu.mil_spec_multiplier = max(0.1, min(3.0, mil_spec))
rpu.restricted_multiplier = max(0.1, min(3.0, restricted))
rpu.classified_multiplier = max(0.1, min(3.0, classified))
rpu.covert_multiplier = max(0.1, min(3.0, covert))
rpu.rare_special_multiplier = max(0.1, min(3.0, rare_special))
rpu.luck_multiplier = max(0.1, min(3.0, luck))
rpu.auto_adjust = auto_adjust
rpu.updated_at = datetime.utcnow()
db.commit()
return JSONResponse({
"success": True,
"message": f"Настройки РПУ для {target_user.username} обновлены",
"rpu_level": calculate_rpu_level(rpu)
})
@admin_router.post("/api/user/{user_id}/rpu/preset")
async def admin_set_rpu_preset(
user_id: int,
preset: str = Form(...),
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
# Теперь: низкий множитель = высокий РПУ (не везёт)
# высокий множитель = низкий РПУ (везёт)
presets = {
"lucky": {"all": 2.0, "luck": 1.8}, # ~10% РПУ - очень везёт
"normal": {"all": 1.0, "luck": 1.0}, # ~50% РПУ - обычно
"unlucky": {"all": 0.6, "luck": 0.7}, # ~75% РПУ - не везёт
"very_unlucky": {"all": 0.3, "luck": 0.4} # ~95% РПУ - проклят
}
if preset not in presets:
return JSONResponse(status_code=400, content={"error": "Неизвестный пресет"})
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
return JSONResponse(status_code=404, content={"error": "Пользователь не найден"})
rpu = get_user_rpu(db, user_id)
p = presets[preset]
rpu.consumer_multiplier = p["all"]
rpu.industrial_multiplier = p["all"]
rpu.mil_spec_multiplier = p["all"]
rpu.restricted_multiplier = p["all"]
rpu.classified_multiplier = p["all"]
rpu.covert_multiplier = p["all"]
rpu.rare_special_multiplier = p["all"]
rpu.luck_multiplier = p["luck"]
rpu.updated_at = datetime.utcnow()
db.commit()
level = calculate_rpu_level(rpu)
desc = get_rpu_description(level)
return JSONResponse({
"success": True,
"message": f"Пресет '{preset}' применен. Уровень РПУ: {level:.0f}% ({desc})"
})
# ========== API КЕЙСЫ ==========
@admin_router.post("/api/cases/create")
async def admin_create_case(
name: str = Form(...),
display_name: str = Form(...),
price: float = Form(...),
description: str = Form(""),
image_url: str = Form(""),
items: str = Form(...),
admin: User = Depends(get_current_admin)
):
cases = load_cases_config()
for case in cases:
if case['name'] == name:
return JSONResponse(status_code=400, content={"error": "Кейс с таким именем уже существует"})
try:
item_ids = json.loads(items)
except:
return JSONResponse(status_code=400, content={"error": "Неверный формат списка предметов"})
new_case = {
"name": name,
"display_name": display_name,
"price_open": price,
"items": item_ids,
"image_url": image_url,
"description": description
}
cases.append(new_case)
save_cases_config(cases)
return JSONResponse({
"success": True,
"case": new_case,
"message": f"Кейс '{display_name}' создан"
})
@admin_router.post("/api/cases/update/{case_name}")
async def admin_update_case(
case_name: str,
display_name: str = Form(...),
price: float = Form(...),
description: str = Form(""),
image_url: str = Form(""),
items: str = Form(...),
admin: User = Depends(get_current_admin)
):
cases = load_cases_config()
for case in cases:
if case['name'] == case_name:
try:
item_ids = json.loads(items)
except:
return JSONResponse(status_code=400, content={"error": "Неверный формат списка предметов"})
case['display_name'] = display_name
case['price_open'] = price
case['description'] = description
case['image_url'] = image_url
case['items'] = item_ids
save_cases_config(cases)
return JSONResponse({
"success": True,
"case": case,
"message": f"Кейс '{display_name}' обновлен"
})
return JSONResponse(status_code=404, content={"error": "Кейс не найден"})
@admin_router.post("/api/cases/delete/{case_name}")
async def admin_delete_case(
case_name: str,
admin: User = Depends(get_current_admin)
):
cases = load_cases_config()
cases = [c for c in cases if c['name'] != case_name]
save_cases_config(cases)
mainpage = load_mainpage_config()
for section in mainpage:
section['cases'] = [c for c in section['cases'] if c != case_name]
save_mainpage_config(mainpage)
return JSONResponse({
"success": True,
"message": f"Кейс '{case_name}' удален"
})
@admin_router.post("/api/mainpage/update")
async def admin_update_mainpage(
config: str = Form(...),
admin: User = Depends(get_current_admin)
):
try:
new_config = json.loads(config)
except:
return JSONResponse(status_code=400, content={"error": "Неверный формат JSON"})
save_mainpage_config(new_config)
return JSONResponse({
"success": True,
"message": "Конфигурация главной страницы обновлена"
})
@admin_router.get("/api/items/search")
async def admin_search_items(
q: str = "",
min_price: float = 0,
max_price: float = 0,
limit: int = 200
):
"""Поиск предметов для добавления в кейс / апгрейда"""
from frontend import ALL_ITEMS
query = q.lower().strip()
results = []
def accept_item(item, idx):
if min_price > 0 and item.get("price_rub", 0) < min_price:
return False
if max_price > 0 and item.get("price_rub", 0) > max_price:
return False
return True
if not query or len(query) < 2:
# Без поиска возвращаем разнообразные предметы со всего ассортимента
# Если есть ценовой фильтр — проходим по всем, иначе шагами
if min_price > 0 or max_price > 0:
for idx, item in enumerate(ALL_ITEMS):
if accept_item(item, idx):
item_id = item.get("_id", idx)
results.append({
"id": item_id,
"name": item.get("market_hash_name", "Unknown"),
"rarity": item.get("rarity", "Unknown"),
"image_url": item.get("image_url", ""),
"price_rub": item.get("price_rub", 0),
"weapon": item.get("weapon", ""),
"pattern": item.get("pattern", "")
})
if len(results) >= limit:
break
else:
step = max(1, len(ALL_ITEMS) // limit)
for idx in range(0, len(ALL_ITEMS), step):
if not accept_item(ALL_ITEMS[idx], idx):
continue
item = ALL_ITEMS[idx]
item_id = item.get("_id", idx)
results.append({
"id": item_id,
"name": item.get("market_hash_name", "Unknown"),
"rarity": item.get("rarity", "Unknown"),
"image_url": item.get("image_url", ""),
"price_rub": item.get("price_rub", 0),
"weapon": item.get("weapon", ""),
"pattern": item.get("pattern", "")
})
if len(results) >= 200:
break
else:
for idx, item in enumerate(ALL_ITEMS):
if not accept_item(item, idx):
continue
name = item.get("market_hash_name", "").lower()
weapon = item.get("weapon", "").lower()
pattern = item.get("pattern", "").lower()
if query in name or query in weapon or query in pattern:
item_id = item.get("_id", idx)
results.append({
"id": item_id,
"name": item.get("market_hash_name", "Unknown"),
"rarity": item.get("rarity", "Unknown"),
"image_url": item.get("image_url", ""),
"price_rub": item.get("price_rub", 0),
"weapon": item.get("weapon", ""),
"pattern": item.get("pattern", "")
})
if len(results) >= 200:
break
return results[:200]
@admin_router.post("/api/mainpage/section/add")
async def admin_add_mainpage_section(
tab: str = Form(...),
admin: User = Depends(get_current_admin)
):
"""Добавить новую секцию на главную"""
mainpage = load_mainpage_config()
# Проверяем, нет ли уже такой секции
for section in mainpage:
if section['tab'] == tab:
return JSONResponse(status_code=400, content={"error": "Секция уже существует"})
mainpage.append({
"tab": tab,
"cases": []
})
save_mainpage_config(mainpage)
return JSONResponse({
"success": True,
"message": f"Секция '{tab}' добавлена"
})
@admin_router.post("/api/mainpage/section/delete")
async def admin_delete_mainpage_section(
tab: str = Form(...),
admin: User = Depends(get_current_admin)
):
"""Удалить секцию с главной"""
mainpage = load_mainpage_config()
mainpage = [s for s in mainpage if s['tab'] != tab]
save_mainpage_config(mainpage)
return JSONResponse({
"success": True,
"message": f"Секция '{tab}' удалена"
})
@admin_router.post("/api/mainpage/section/add-case")
async def admin_add_case_to_section(
tab: str = Form(...),
case_name: str = Form(...),
admin: User = Depends(get_current_admin)
):
"""Добавить кейс в секцию"""
mainpage = load_mainpage_config()
for section in mainpage:
if section['tab'] == tab:
if case_name not in section['cases']:
section['cases'].append(case_name)
save_mainpage_config(mainpage)
return JSONResponse({
"success": True,
"message": f"Кейс '{case_name}' добавлен в секцию '{tab}'"
})
else:
return JSONResponse(status_code=400, content={"error": "Кейс уже в секции"})
return JSONResponse(status_code=404, content={"error": "Секция не найдена"})
@admin_router.post("/api/mainpage/section/remove-case")
async def admin_remove_case_from_section(
tab: str = Form(...),
case_name: str = Form(...),
admin: User = Depends(get_current_admin)
):
"""Удалить кейс из секции"""
mainpage = load_mainpage_config()
for section in mainpage:
if section['tab'] == tab:
if case_name in section['cases']:
section['cases'].remove(case_name)
save_mainpage_config(mainpage)
return JSONResponse({
"success": True,
"message": f"Кейс '{case_name}' удален из секции '{tab}'"
})
return JSONResponse(status_code=404, content={"error": "Секция или кейс не найдены"})
@admin_router.get("/api/cases/list")
async def admin_get_cases_list(
admin: User = Depends(get_current_admin)
):
"""Получить список всех кейсов для выбора"""
cases = load_cases_config()
return [{"name": c['name'], "display_name": c.get('display_name', c['name'])} for c in cases]
@admin_router.get("/api/items/{item_id}")
async def admin_get_item(
item_id: int,
admin: User = Depends(get_current_admin)
):
"""Получить информацию о предмете по ID"""
from frontend import ALL_ITEMS
for idx, item in enumerate(ALL_ITEMS):
if item.get("_id", idx) == item_id:
return {
"id": item_id,
"name": item.get("market_hash_name", "Unknown"),
"rarity": item.get("rarity", "Unknown"),
"image_url": item.get("image_url", ""),
"price_rub": item.get("price_rub", 0),
"weapon": item.get("weapon", ""),
"pattern": item.get("pattern", ""),
"type": item.get("type", "Normal"),
"wear": item.get("wear", "")
}
return JSONResponse(status_code=404, content={"error": "Предмет не найден"})
@admin_router.post("/api/reset-password/{user_id}")
async def admin_reset_password(
user_id: int,
new_password: str = Form(...),
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
"""Сброс пароля пользователя (только для админов)"""
from auth import get_password_hash
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
return JSONResponse(status_code=404, content={"error": "Пользователь не найден"})
target_user.hashed_password = get_password_hash(new_password)
db.commit()
return JSONResponse({
"success": True,
"message": f"Пароль для {target_user.username} изменен"
})