CloudShare/bank.py

6070 lines
236 KiB
Python

from flask import Flask, request, redirect, session, Response, jsonify
import json, os, random, time, uuid, re, html, base64, hashlib, hmac, struct
from datetime import datetime
from functools import wraps
from urllib.parse import quote
app = Flask(__name__)
app.secret_key = "bank_secret_key_2025"
app.config['SESSION_PERMANENT'] = True
app.config['PERMANENT_SESSION_LIFETIME'] = 3600
USERS_FILE = "users.json"
HISTORY_FILE = "history.json"
SHOP_FILE = "shop.json"
PAYMENTS_FILE = "payments.json" # Новый файл для платежей
JOURNAL_FILE = "journal.json"
CLASSES_FILE = "classes.json"
SUBJECTS_FILE = "subjects.json"
PARENT_LINKS_FILE = "parent_links.json"
CRYPTO_FILE = "crypto.json"
SERKRIPTO_NAME = "SerKripto"
SERKRIPTO_SYMBOL = "SERK"
ROLE_ADMIN = "admin"
ROLE_MANAGER = "manager"
ROLE_TEACHER = "teacher"
ROLE_PARENT = "parent"
ROLE_STUDENT = "student"
ROLE_CLIENT = "client"
VALID_ROLES = {
ROLE_ADMIN,
ROLE_MANAGER,
ROLE_TEACHER,
ROLE_PARENT,
ROLE_STUDENT,
ROLE_CLIENT
}
FINANCE_ROLES = (ROLE_ADMIN, ROLE_MANAGER, ROLE_TEACHER, ROLE_PARENT, ROLE_STUDENT, ROLE_CLIENT)
CREDIT_ROLES = (ROLE_ADMIN, ROLE_MANAGER, ROLE_TEACHER, ROLE_PARENT, ROLE_CLIENT)
CRYPTO_ROLES = (ROLE_ADMIN, ROLE_MANAGER, ROLE_TEACHER, ROLE_PARENT, ROLE_CLIENT)
SHOP_SELL_ROLES = (ROLE_ADMIN, ROLE_MANAGER, ROLE_TEACHER, ROLE_PARENT, ROLE_CLIENT)
JOURNAL_ROLES = (ROLE_ADMIN, ROLE_TEACHER)
TOTP_DIGITS = 6
TOTP_PERIOD_SEC = 30
TOTP_DRIFT_WINDOWS = 1
PENDING_2FA_TTL_SEC = 300
def normalize_role(role_value):
role = str(role_value or ROLE_CLIENT).strip().lower()
if role not in VALID_ROLES:
return ROLE_CLIENT
return role
def parse_bool(value):
if isinstance(value, bool):
return value
if isinstance(value, (int, float)):
return value != 0
if isinstance(value, str):
return value.strip().lower() in {"1", "true", "yes", "on"}
return False
def normalize_totp_secret(secret_value):
cleaned = re.sub(r"[^A-Z2-7]", "", str(secret_value or "").upper())
return cleaned
def generate_totp_secret(length=32):
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
return "".join(random.choice(alphabet) for _ in range(max(16, int(length))))
def generate_totp_code(secret, for_timestamp=None):
normalized_secret = normalize_totp_secret(secret)
if not normalized_secret:
return None
pad_len = (8 - (len(normalized_secret) % 8)) % 8
padded = normalized_secret + ("=" * pad_len)
try:
key = base64.b32decode(padded, casefold=True)
except Exception:
return None
timestamp = int(time.time()) if for_timestamp is None else int(for_timestamp)
counter = int(timestamp // TOTP_PERIOD_SEC)
counter_bytes = struct.pack(">Q", counter)
digest = hmac.new(key, counter_bytes, hashlib.sha1).digest()
offset = digest[-1] & 0x0F
binary = (
((digest[offset] & 0x7F) << 24)
| ((digest[offset + 1] & 0xFF) << 16)
| ((digest[offset + 2] & 0xFF) << 8)
| (digest[offset + 3] & 0xFF)
)
return str(binary % (10 ** TOTP_DIGITS)).zfill(TOTP_DIGITS)
def verify_totp_code(secret, raw_code):
code = re.sub(r"\D", "", str(raw_code or ""))
if len(code) != TOTP_DIGITS:
return False
now = int(time.time())
for offset in range(-TOTP_DRIFT_WINDOWS, TOTP_DRIFT_WINDOWS + 1):
check_time = now + (offset * TOTP_PERIOD_SEC)
expected = generate_totp_code(secret, check_time)
if expected and code == expected:
return True
return False
def get_totp_uri(username, secret):
issuer = quote("Bank School")
account = quote(str(username or "user"))
return f"otpauth://totp/{issuer}:{account}?secret={secret}&issuer={issuer}&digits={TOTP_DIGITS}&period={TOTP_PERIOD_SEC}"
def clear_pending_2fa_session():
session.pop("pending_2fa_user", None)
session.pop("pending_2fa_started", None)
def is_pending_2fa_alive():
started = int(session.get("pending_2fa_started", 0) or 0)
if started <= 0:
return False
return (int(time.time()) - started) <= PENDING_2FA_TTL_SEC
def get_user_role(user_data):
if not isinstance(user_data, dict):
return ROLE_CLIENT
return normalize_role(user_data.get("role", ROLE_CLIENT))
def has_role(user_data, allowed_roles):
role = get_user_role(user_data)
normalized_allowed = {normalize_role(r) for r in allowed_roles}
return role in normalized_allowed
def default_admin_user():
return {
"password": "CreateSergeyPass",
"balance": 0,
"deposit": 0,
"credit": 0,
"serkripto": 0.0,
"role": "admin",
"full_name": "Администратор Системы",
"email": "kompania.bank@gmail.com",
"api_key": "Sergey_API_KEY_1",
"twofa_enabled": False,
"twofa_secret": "",
}
# ---------- ИНИЦИАЛИЗАЦИЯ ФАЙЛОВ ----------
def init_files():
"""Инициализация файлов (без удаления существующих)"""
# Создаем файлы если они не существуют
files_to_init = [
(USERS_FILE, {"Sergey": default_admin_user()}),
(HISTORY_FILE, []),
(SHOP_FILE, {"items": [], "ads": []}),
(PAYMENTS_FILE, []), # Инициализация файла платежей
(JOURNAL_FILE, {}),
(CLASSES_FILE, {}),
(SUBJECTS_FILE, []),
(PARENT_LINKS_FILE, {}),
(CRYPTO_FILE, {
"name": SERKRIPTO_NAME,
"symbol": SERKRIPTO_SYMBOL,
"price": 100.0,
"min_price": 1.0,
"max_price": 1000000.0,
"price_step_buy": 0.012,
"price_step_sell": 0.01,
"idle_decay_rate": 0.0015,
"idle_decay_interval_sec": 30,
"last_buy_at": int(time.time()),
"last_update": int(time.time()),
"stats": {
"buy_ops": 0,
"sell_ops": 0,
"buy_volume": 0.0,
"sell_volume": 0.0
}
})
]
for filename, default_data in files_to_init:
if not os.path.exists(filename):
with open(filename, "w", encoding='utf-8') as f:
json.dump(default_data, f, indent=4, ensure_ascii=False)
# Инициализируем файлы при запуске
init_files()
# Совместимые no-op хуки уведомлений
def ws_emit_system(event_name, payload=None):
return None
def ws_emit_user(username, event_name, payload=None):
return None
def ws_emit_balance_update(username):
return None
# ---------- ФУНКЦИИ ДЛЯ РАБОТЫ С ДАННЫМИ ----------
def load_users():
try:
with open(USERS_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
# Убеждаемся, что данные в правильном формате
if not isinstance(data, dict):
print("Warning: users.json is not a dictionary, resetting to default")
data = {"admin": default_admin_user()}
save_users(data)
return data
# Проверяем, что все значения являются словарями
cleaned_data = {}
for username, user_data in data.items():
if isinstance(user_data, dict):
user_data["role"] = get_user_role(user_data)
try:
user_data["serkripto"] = round(float(user_data.get("serkripto", 0.0)), 6)
except:
user_data["serkripto"] = 0.0
user_data["twofa_secret"] = normalize_totp_secret(user_data.get("twofa_secret", ""))
user_data["twofa_enabled"] = parse_bool(user_data.get("twofa_enabled", False))
if not user_data["twofa_secret"]:
user_data["twofa_enabled"] = False
cleaned_data[username] = user_data
else:
print(f"Warning: User {username} data is not a dict, resetting")
if username == "admin":
cleaned_data[username] = default_admin_user()
else:
cleaned_data[username] = {
"password": "reset123",
"balance": 1000,
"deposit": 0,
"credit": 0,
"serkripto": 0.0,
"role": "client",
"full_name": "Пользователь",
"email": "user@example.com",
"api_key": None,
"twofa_enabled": False,
"twofa_secret": "",
}
# Гарантируем наличие рабочего admin даже после ручной порчи users.json
admin_defaults = default_admin_user()
admin_user = cleaned_data.get("admin")
if not isinstance(admin_user, dict):
cleaned_data["admin"] = admin_defaults
else:
for key, value in admin_defaults.items():
admin_user.setdefault(key, value)
admin_user["role"] = ROLE_ADMIN
if not admin_user.get("password"):
admin_user["password"] = admin_defaults["password"]
if cleaned_data != data:
save_users(cleaned_data)
return cleaned_data
except Exception as e:
print(f"Error loading users: {e}")
data = {"admin": default_admin_user()}
save_users(data)
return data
def save_users(data):
with open(USERS_FILE, "w", encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False)
def load_history():
try:
with open(HISTORY_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except:
return []
def save_history(data):
with open(HISTORY_FILE, "w", encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False)
# ---------- ЭЛЕКТРОННЫЙ ДНЕВНИК ----------
def load_journal():
try:
with open(JOURNAL_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
return data if isinstance(data, dict) else {}
except:
return {}
def save_journal(data):
with open(JOURNAL_FILE, "w", encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False)
def load_classes():
try:
with open(CLASSES_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
return data if isinstance(data, dict) else {}
except:
return {}
def save_classes(data):
with open(CLASSES_FILE, "w", encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False)
def load_subjects():
try:
with open(SUBJECTS_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
return data if isinstance(data, list) else []
except:
return []
def save_subjects(data):
with open(SUBJECTS_FILE, "w", encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False)
def load_parent_links():
try:
with open(PARENT_LINKS_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
return data if isinstance(data, dict) else {}
except:
return {}
def save_parent_links(data):
with open(PARENT_LINKS_FILE, "w", encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False)
def get_parent_children(parent_username):
links = load_parent_links()
children = links.get(parent_username, [])
if not isinstance(children, list):
return []
unique_children = []
for child in children:
child_name = str(child).strip()
if child_name and child_name not in unique_children:
unique_children.append(child_name)
return unique_children
def calculate_money_for_grade(grade):
grade = int(grade)
mapping = {
5: 20,
4: 10,
3: -10,
2: -20
}
return mapping.get(grade, 0)
# ---------- ФУНКЦИИ ДЛЯ ПЛАТЕЖЕЙ ----------
def load_payments():
"""Загрузка платежей"""
try:
with open(PAYMENTS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except:
return []
def save_payments(data):
"""Сохранение платежей"""
with open(PAYMENTS_FILE, "w", encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False)
def get_payment_by_id(payment_id):
payments = load_payments()
for payment in payments:
if payment.get("id") == payment_id:
return payment
return None
def sanitize_payment_output(payment):
if not isinstance(payment, dict):
return payment
safe_payment = payment.copy()
balances = safe_payment.get("balances")
if isinstance(balances, dict):
safe_payment["balances"] = {
"payer_before": balances.get("payer_before"),
"payer_after": balances.get("payer_after")
}
return safe_payment
def ws_emit_payment_event(event_name, payment, extra_payload=None):
if not isinstance(payment, dict):
return
payload = {
"id": payment.get("id"),
"status": payment.get("status"),
"payer": payment.get("payer"),
"receiver": payment.get("receiver"),
"amount": payment.get("amount"),
"allow_any_payer": bool(payment.get("allow_any_payer", False)),
"created_by": payment.get("created_by")
}
if isinstance(extra_payload, dict):
payload.update(extra_payload)
ws_emit_system(event_name, payload)
notified_users = set()
for username in [payment.get("payer"), payment.get("receiver"), payment.get("created_by")]:
username = str(username or "").strip()
if not username or username == "*" or username in notified_users:
continue
ws_emit_user(username, event_name, payload)
notified_users.add(username)
def ws_emit_crypto_market_event(market, reason="update"):
if not isinstance(market, dict):
return
payload = {
"reason": reason,
"coin": market.get("name", SERKRIPTO_NAME),
"symbol": market.get("symbol", SERKRIPTO_SYMBOL),
"price_rub": round(float(market.get("price", 0.0)), 4),
"min_price_rub": round(float(market.get("min_price", 1.0)), 4),
"max_price_rub": round(float(market.get("max_price", 1000000.0)), 4),
"time": int(time.time())
}
ws_emit_system("crypto.market", payload)
def create_payment(payer, receiver, amount, description="", external_id="", allow_any_payer=False, created_by=None):
"""Создание платежа"""
payments = load_payments()
payment_id = str(uuid.uuid4())[:12]
safe_payer = "*" if allow_any_payer else payer
payment_data = {
"id": payment_id,
"payer": safe_payer,
"receiver": receiver,
"amount": int(amount),
"description": description,
"external_id": external_id,
"allow_any_payer": bool(allow_any_payer),
"created_by": created_by if created_by else payer,
"created": int(time.time()),
"status": "pending", # pending, completed, failed, cancelled
"completed_at": None
}
payments.append(payment_data)
save_payments(payments)
ws_emit_payment_event("payment.created", payment_data)
return payment_id, payment_data
def process_payment(payment_id, payer_username=None):
"""Обработка платежа"""
payments = load_payments()
users = load_users()
payment = None
for p in payments:
if p["id"] == payment_id:
payment = p
break
if not payment:
return False, "Платеж не найден"
if payment["status"] != "pending":
return False, f"Платеж уже обработан (статус: {payment['status']})"
receiver = payment["receiver"]
amount = payment["amount"]
allow_any_payer = bool(payment.get("allow_any_payer", False))
if allow_any_payer:
if not payer_username:
return False, "Для этого платежа нужен логин плательщика"
payer = payer_username
else:
payer = payment["payer"]
if payer_username and payer != payer_username:
return False, "Неверный плательщик"
# Проверяем существование пользователей
if payer not in users:
return False, "Плательщик не найден"
if receiver not in users:
return False, "Получатель не найден"
if payer == receiver:
return False, "Нельзя оплатить самому себе"
# Проверяем баланс плательщика
if users[payer]["balance"] < amount:
return False, "Недостаточно средств"
payer_balance_before = users[payer]["balance"]
receiver_balance_before = users[receiver]["balance"]
# Выполняем перевод
users[payer]["balance"] -= amount
users[receiver]["balance"] += amount
save_users(users)
payer_balance_after = users[payer]["balance"]
receiver_balance_after = users[receiver]["balance"]
# Обновляем статус платежа
for p in payments:
if p["id"] == payment_id:
if allow_any_payer:
p["payer"] = payer
p["status"] = "completed"
p["completed_at"] = int(time.time())
p["balances"] = {
"payer_before": payer_balance_before,
"payer_after": payer_balance_after,
"receiver_before": receiver_balance_before,
"receiver_after": receiver_balance_after
}
break
save_payments(payments)
# Добавляем в историю
add_history(payer, "payment_sent", amount, f"Платеж #{payment_id} для {receiver}")
add_history(receiver, "payment_received", amount, f"Платеж #{payment_id} от {payer}")
ws_emit_payment_event("payment.completed", payment, {
"payer_before": payer_balance_before,
"payer_after": payer_balance_after
})
return True, "Платеж успешно выполнен"
def cancel_payment(payment_id):
"""Отмена платежа"""
payments = load_payments()
for payment in payments:
if payment["id"] == payment_id and payment["status"] == "pending":
payment["status"] = "cancelled"
save_payments(payments)
ws_emit_payment_event("payment.cancelled", payment)
return True, "Платеж отменен"
return False, "Платеж не найден или уже обработан"
# ---------- ФУНКЦИИ ДЛЯ МАГАЗИНА ----------
def load_shop():
"""Загрузка данных магазина"""
try:
with open(SHOP_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
# Обеспечиваем наличие всех ключей
if "items" not in data:
data["items"] = []
if "ads" not in data:
data["ads"] = []
return data
except:
return {"items": [], "ads": []}
def save_shop(data):
"""Сохранение данных магазина"""
with open(SHOP_FILE, "w", encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False)
def create_shop_item(seller_username, title, description, price, category="other"):
"""Создание товара в магазине"""
shop_data = load_shop()
item_id = str(uuid.uuid4())[:8]
new_item = {
"id": item_id,
"seller": seller_username,
"title": title,
"description": description,
"price": int(price),
"category": category,
"created": int(time.time()),
"status": "available",
"buyer": None
}
shop_data["items"].append(new_item)
save_shop(shop_data)
# Добавляем в историю
add_history(seller_username, "create_shop_item", 0, f"Товар: {title}")
ws_emit_system("shop.item_created", {
"item_id": item_id,
"seller": seller_username,
"title": title,
"price": int(price),
"category": category
})
ws_emit_user(seller_username, "shop.item_created", {
"item_id": item_id,
"title": title,
"price": int(price)
})
return item_id
def create_advertisement(author, title, content, contact_info):
"""Создание объявления"""
shop_data = load_shop()
ad_id = str(uuid.uuid4())[:8]
new_ad = {
"id": ad_id,
"author": author,
"title": title,
"content": content,
"contact_info": contact_info,
"created": int(time.time()),
"status": "active"
}
shop_data["ads"].append(new_ad)
save_shop(shop_data)
# Добавляем в историю
add_history(author, "create_advertisement", 0, f"Объявление: {title}")
ws_emit_system("shop.ad_created", {
"ad_id": ad_id,
"author": author,
"title": title
})
ws_emit_user(author, "shop.ad_created", {
"ad_id": ad_id,
"title": title
})
return ad_id
def buy_item(item_id, buyer_username):
"""Покупка товара"""
shop_data = load_shop()
users = load_users()
# Находим товар
item_to_buy = None
for item in shop_data["items"]:
if item["id"] == item_id and item["status"] == "available":
item_to_buy = item
break
if not item_to_buy:
return False, "Товар не найден или уже продан"
seller = item_to_buy["seller"]
price = item_to_buy["price"]
# Проверяем, есть ли у покупателя достаточно средств
if buyer_username not in users:
return False, "Покупатель не найден"
if users[buyer_username]["balance"] < price:
return False, "Недостаточно средств"
# Проверяем, что покупатель не покупает у себя
if buyer_username == seller:
return False, "Нельзя купить собственный товар"
# Проверяем, существует ли продавец
if seller not in users:
return False, "Продавец не найден"
# Выполняем транзакцию
users[buyer_username]["balance"] -= price
users[seller]["balance"] += price
save_users(users)
# Обновляем статус товара
for item in shop_data["items"]:
if item["id"] == item_id:
item["status"] = "sold"
item["buyer"] = buyer_username
item["sold_date"] = int(time.time())
break
save_shop(shop_data)
# Добавляем в историю
add_history(buyer_username, "buy_item", price, f"Товар: {item_to_buy['title']} от {seller}")
add_history(seller, "sell_item", price, f"Товар: {item_to_buy['title']} покупателю {buyer_username}")
ws_emit_system("shop.item_sold", {
"item_id": item_id,
"title": item_to_buy["title"],
"seller": seller,
"buyer": buyer_username,
"price": price
})
ws_emit_user(buyer_username, "shop.item_sold", {
"item_id": item_id,
"title": item_to_buy["title"],
"price": price,
"role": "buyer"
})
ws_emit_user(seller, "shop.item_sold", {
"item_id": item_id,
"title": item_to_buy["title"],
"price": price,
"role": "seller"
})
return True, "Покупка успешно завершена"
def default_crypto_market():
now = int(time.time())
return {
"name": SERKRIPTO_NAME,
"symbol": SERKRIPTO_SYMBOL,
"price": 100.0,
"min_price": 1.0,
"max_price": 1000000.0,
"price_step_buy": 0.012,
"price_step_sell": 0.01,
"idle_decay_rate": 0.0015,
"idle_decay_interval_sec": 30,
"last_buy_at": now,
"last_update": now,
"stats": {
"buy_ops": 0,
"sell_ops": 0,
"buy_volume": 0.0,
"sell_volume": 0.0
}
}
def normalize_crypto_market(data):
defaults = default_crypto_market()
market = {}
if not isinstance(data, dict):
data = {}
market["name"] = str(data.get("name", defaults["name"]) or defaults["name"])
market["symbol"] = str(data.get("symbol", defaults["symbol"]) or defaults["symbol"]).upper()
def to_float(value, fallback):
try:
return float(value)
except:
return float(fallback)
def to_int(value, fallback):
try:
return int(value)
except:
return int(fallback)
market["min_price"] = max(0.01, to_float(data.get("min_price"), defaults["min_price"]))
market["max_price"] = max(market["min_price"], to_float(data.get("max_price"), defaults["max_price"]))
market["price"] = to_float(data.get("price"), defaults["price"])
market["price"] = min(market["max_price"], max(market["min_price"], market["price"]))
market["price"] = round(market["price"], 4)
market["price_step_buy"] = min(0.5, max(0.0, to_float(data.get("price_step_buy"), defaults["price_step_buy"])))
market["price_step_sell"] = min(0.5, max(0.0, to_float(data.get("price_step_sell"), defaults["price_step_sell"])))
market["idle_decay_rate"] = min(0.2, max(0.0, to_float(data.get("idle_decay_rate"), defaults["idle_decay_rate"])))
market["idle_decay_interval_sec"] = max(5, to_int(data.get("idle_decay_interval_sec"), defaults["idle_decay_interval_sec"]))
market["last_buy_at"] = to_int(data.get("last_buy_at"), defaults["last_buy_at"])
market["last_update"] = to_int(data.get("last_update"), defaults["last_update"])
stats = data.get("stats", {})
if not isinstance(stats, dict):
stats = {}
market["stats"] = {
"buy_ops": max(0, to_int(stats.get("buy_ops"), 0)),
"sell_ops": max(0, to_int(stats.get("sell_ops"), 0)),
"buy_volume": max(0.0, round(to_float(stats.get("buy_volume"), 0.0), 6)),
"sell_volume": max(0.0, round(to_float(stats.get("sell_volume"), 0.0), 6))
}
return market
def load_crypto_market():
need_save = False
raw = None
try:
with open(CRYPTO_FILE, 'r', encoding='utf-8') as f:
raw = json.load(f)
except:
raw = default_crypto_market()
need_save = True
market = normalize_crypto_market(raw)
if raw != market:
need_save = True
if need_save:
save_crypto_market(market)
return market
def save_crypto_market(data):
with open(CRYPTO_FILE, "w", encoding='utf-8') as f:
json.dump(normalize_crypto_market(data), f, indent=4, ensure_ascii=False)
def apply_serkripto_idle_decay(market):
market = normalize_crypto_market(market)
now = int(time.time())
last_update = int(market.get("last_update", now))
interval = int(market.get("idle_decay_interval_sec", 30))
if now <= last_update:
return market, False
if now - int(market.get("last_buy_at", 0)) < interval:
market["last_update"] = now
return market, False
steps = (now - last_update) // interval
if steps <= 0:
return market, False
price_before = float(market.get("price", 100.0))
decay_rate = float(market.get("idle_decay_rate", 0.0015))
min_price = float(market.get("min_price", 1.0))
decayed_price = max(min_price, price_before * ((1 - decay_rate) ** steps))
market["price"] = round(decayed_price, 4)
market["last_update"] = now
changed = abs(price_before - market["price"]) > 1e-9
return market, changed
def get_user_serkripto_amount(user_data):
if not isinstance(user_data, dict):
return 0.0
try:
return round(max(0.0, float(user_data.get("serkripto", 0.0))), 6)
except:
return 0.0
def buy_serkripto(username, rub_amount):
try:
amount = int(rub_amount)
except:
return False, "invalid_amount"
if amount <= 0:
return False, "invalid_amount"
users = load_users()
user_data = users.get(username)
if not isinstance(user_data, dict):
return False, "user_not_found"
market = load_crypto_market()
market, decayed = apply_serkripto_idle_decay(market)
if decayed:
save_crypto_market(market)
ws_emit_crypto_market_event(market, "idle_decay")
price_before = float(market.get("price", 0.0))
if price_before <= 0:
return False, "market_unavailable"
if int(user_data.get("balance", 0)) < amount:
return False, "insufficient_funds"
quantity = round(amount / price_before, 6)
if quantity <= 0:
return False, "amount_too_small"
user_data["balance"] = int(user_data.get("balance", 0)) - amount
user_data["serkripto"] = round(get_user_serkripto_amount(user_data) + quantity, 6)
save_users(users)
growth = min(0.25, quantity * float(market.get("price_step_buy", 0.012)))
max_price = float(market.get("max_price", 1000000.0))
price_after = min(max_price, price_before * (1 + growth))
now = int(time.time())
market["price"] = round(price_after, 4)
market["last_buy_at"] = now
market["last_update"] = now
market["stats"]["buy_ops"] = int(market["stats"].get("buy_ops", 0)) + 1
market["stats"]["buy_volume"] = round(float(market["stats"].get("buy_volume", 0.0)) + quantity, 6)
save_crypto_market(market)
ws_emit_crypto_market_event(market, "buy")
add_history(username, "crypto_buy", amount, f"{SERKRIPTO_NAME} +{quantity:.6f} по {price_before:.4f}")
ws_emit_balance_update(username)
ws_emit_user(username, "crypto.trade", {
"side": "buy",
"coin": SERKRIPTO_NAME,
"quantity": quantity,
"amount_rub": amount,
"price_before": round(price_before, 4),
"price_after": round(market["price"], 4)
})
ws_emit_system("crypto.trade", {
"side": "buy",
"user": username,
"coin": SERKRIPTO_NAME,
"quantity": quantity,
"price": round(market["price"], 4)
})
return True, {
"coin": SERKRIPTO_NAME,
"symbol": market.get("symbol", SERKRIPTO_SYMBOL),
"quantity": quantity,
"amount_rub": amount,
"price_before": round(price_before, 4),
"price_after": round(float(market.get("price", price_before)), 4),
"balance_rub": int(user_data.get("balance", 0)),
"wallet_quantity": get_user_serkripto_amount(user_data)
}
def sell_serkripto(username, quantity):
try:
qty = round(float(quantity), 6)
except:
return False, "invalid_quantity"
if qty <= 0:
return False, "invalid_quantity"
users = load_users()
user_data = users.get(username)
if not isinstance(user_data, dict):
return False, "user_not_found"
wallet_qty = get_user_serkripto_amount(user_data)
if wallet_qty + 1e-9 < qty:
return False, "insufficient_crypto"
market = load_crypto_market()
market, decayed = apply_serkripto_idle_decay(market)
if decayed:
save_crypto_market(market)
ws_emit_crypto_market_event(market, "idle_decay")
price_before = float(market.get("price", 0.0))
if price_before <= 0:
return False, "market_unavailable"
payout = int(round(qty * price_before))
if payout <= 0:
return False, "amount_too_small"
user_data["serkripto"] = round(max(0.0, wallet_qty - qty), 6)
user_data["balance"] = int(user_data.get("balance", 0)) + payout
save_users(users)
drop = min(0.25, qty * float(market.get("price_step_sell", 0.01)))
min_price = float(market.get("min_price", 1.0))
price_after = max(min_price, price_before * (1 - drop))
now = int(time.time())
market["price"] = round(price_after, 4)
market["last_update"] = now
market["stats"]["sell_ops"] = int(market["stats"].get("sell_ops", 0)) + 1
market["stats"]["sell_volume"] = round(float(market["stats"].get("sell_volume", 0.0)) + qty, 6)
save_crypto_market(market)
ws_emit_crypto_market_event(market, "sell")
add_history(username, "crypto_sell", payout, f"{SERKRIPTO_NAME} -{qty:.6f} по {price_before:.4f}")
ws_emit_balance_update(username)
ws_emit_user(username, "crypto.trade", {
"side": "sell",
"coin": SERKRIPTO_NAME,
"quantity": qty,
"amount_rub": payout,
"price_before": round(price_before, 4),
"price_after": round(market["price"], 4)
})
ws_emit_system("crypto.trade", {
"side": "sell",
"user": username,
"coin": SERKRIPTO_NAME,
"quantity": qty,
"price": round(market["price"], 4)
})
return True, {
"coin": SERKRIPTO_NAME,
"symbol": market.get("symbol", SERKRIPTO_SYMBOL),
"quantity": qty,
"amount_rub": payout,
"price_before": round(price_before, 4),
"price_after": round(float(market.get("price", price_before)), 4),
"balance_rub": int(user_data.get("balance", 0)),
"wallet_quantity": get_user_serkripto_amount(user_data)
}
# ---------- API КЛЮЧИ ----------
def generate_api_key():
"""Генерация API ключа"""
return f"bank_api_{uuid.uuid4().hex[:16]}"
def get_user_by_api_key(api_key):
"""Получение пользователя по API ключу - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
if not api_key:
return None, None
users = load_users()
for username, user_data in users.items():
# Проверяем, что user_data является словарем
if isinstance(user_data, dict) and user_data.get('api_key') == api_key:
return username, user_data
return None, None
# ---------- БАНКОВСКИЕ ОПЕРАЦИИ ----------
def daily_update():
"""Ежедневное обновление депозитов и кредитов"""
users = load_users()
updated = False
updated_users = []
for user_id, u in users.items():
if isinstance(u, dict): # Проверяем, что это словарь
if u.get('deposit', 0) > 0:
u['deposit'] = int(u['deposit'] * 1.01)
updated = True
updated_users.append(user_id)
if u.get('credit', 0) > 0:
u['credit'] = int(u['credit'] * 1.05)
updated = True
if user_id not in updated_users:
updated_users.append(user_id)
if updated:
save_users(users)
ws_emit_system("bank.daily_update", {"users": updated_users})
for username in updated_users:
ws_emit_balance_update(username)
def add_history(user, action, amount, target=None):
"""Добавление записи в историю"""
hist = load_history()
history_item = {
"time": int(time.time()),
"user": user,
"action": action,
"amount": amount,
"target": target,
"id": str(uuid.uuid4())[:8]
}
hist.append(history_item)
save_history(hist)
ws_emit_user(user, "history.new", history_item)
ws_emit_system("history.new", {
"user": user,
"action": action,
"amount": amount,
"target": target
})
ws_emit_balance_update(user)
# ---------- ДЕКОРАТОРЫ ----------
def login_required(f):
"""Декоратор для проверки авторизации"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user' not in session:
return redirect('/')
return f(*args, **kwargs)
return decorated_function
def web_roles_required(*allowed_roles):
"""Проверка ролей для WEB-маршрутов (требует активную сессию)"""
normalized_allowed = {normalize_role(r) for r in allowed_roles}
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user' not in session:
return redirect('/')
users = load_users()
user_data = users.get(session['user'])
if not isinstance(user_data, dict):
return redirect('/')
user_role = get_user_role(user_data)
if user_role not in normalized_allowed:
return redirect('/dashboard')
return f(*args, **kwargs)
return decorated_function
return decorator
def admin_required(f):
"""Декоратор для проверки прав администратора"""
return web_roles_required(ROLE_ADMIN)(f)
# ИСПРАВЛЕННЫЙ ДЕКОРАТОР API
def api_key_required(f):
"""Декоратор для проверки API ключа - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
@wraps(f)
def decorated_function(*args, **kwargs):
api_key = None
# 1. Проверяем заголовок X-API-Key
if 'X-API-Key' in request.headers:
api_key = request.headers.get('X-API-Key')
# 2. Проверяем параметр запроса api_key
if not api_key and request.args.get('api_key'):
api_key = request.args.get('api_key')
# 3. Проверяем JSON тело
if not api_key and request.is_json:
try:
data = request.get_json()
if data and 'api_key' in data:
api_key = data.get('api_key')
except:
pass
# 4. Проверяем форму
if not api_key and request.form.get('api_key'):
api_key = request.form.get('api_key')
if not api_key:
return jsonify({'success': False, 'error': 'API key required'}), 401
# Получаем пользователя по API ключу
username, user_data = get_user_by_api_key(api_key)
if not username:
return jsonify({'success': False, 'error': 'Invalid API key'}), 401
# Сохраняем данные пользователя в объекте request
request.username = username
request.user_data = user_data
request.user_role = get_user_role(user_data)
return f(*args, **kwargs)
return decorated_function
def api_roles_required(*allowed_roles):
"""Проверка ролей для API маршрутов (использовать после api_key_required)"""
normalized_allowed = {normalize_role(r) for r in allowed_roles}
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
user_role = getattr(request, "user_role", get_user_role(getattr(request, "user_data", {})))
if user_role not in normalized_allowed:
return jsonify({
"success": False,
"error": "Access denied for your role",
"role": user_role
}), 403
return f(*args, **kwargs)
return decorated_function
return decorator
# ---------- HTML ШАБЛОНЫ ----------
def render_login(error_message="", twofa_mode=False, username_hint=""):
safe_error = html.escape(str(error_message or "").strip())
alert_html = f'<div class="alert alert-danger mt-3">{safe_error}</div>' if safe_error else ""
safe_username_hint = html.escape(str(username_hint or "").strip())
security_notice_html = '<div class="alert alert-warning mt-3 mb-0 small"><strong>Важно:</strong> Мы не гарантируем полную безопасность сервиса. Все пароли хранятся в открытом виде, в базе данных, всем этим заведует один человек, поэтому я не гарантирую полную безопасность. Если понадобится восстановление, пишите на почту kompania.bank@gmail.com.</div>'
subtitle = "Вход в систему"
auth_form_html = '''
<form method="post">
<div class="mb-3">
<label class="form-label">Логин</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-user"></i></span>
<input type="text" name="username" class="form-control" placeholder="Введите логин" required>
</div>
</div>
<div class="mb-4">
<label class="form-label">Пароль</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
<input type="password" name="password" class="form-control" placeholder="Введите пароль" required>
</div>
</div>
<button type="submit" class="btn btn-primary w-100 mb-3">
<i class="fas fa-sign-in-alt me-2"></i>Войти
</button>
<div class="text-center">
<p class="mb-0">Нет аккаунта? <a href="/register" class="text-decoration-none">Зарегистрироваться</a></p>
</div>
</form>
'''
if twofa_mode:
subtitle = f'Подтверждение входа: <b>{safe_username_hint or "пользователь"}</b>'
auth_form_html = '''
<form method="post">
<input type="hidden" name="login_step" value="2fa">
<div class="alert alert-info">
<i class="fas fa-shield-alt me-2"></i>
Введите 6-значный код из приложения-аутентификатора
</div>
<div class="mb-4">
<label class="form-label">Код 2FA</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-mobile-alt"></i></span>
<input type="text" name="twofa_code" class="form-control" placeholder="123456" inputmode="numeric" autocomplete="one-time-code" maxlength="6" pattern="[0-9]{6}" required autofocus>
</div>
</div>
<button type="submit" class="btn btn-primary w-100 mb-3">
<i class="fas fa-check-circle me-2"></i>Подтвердить вход
</button>
<div class="text-center">
<p class="mb-0"><a href="/?reset_2fa=1" class="text-decoration-none">Войти под другим пользователем</a></p>
</div>
</form>
'''
page = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Банк "Школьный" | Вход в систему</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
:root {
--primary: #4361ee;
--secondary: #3a0ca3;
--gradient: linear-gradient(135deg, #4361ee 0%, #3a0ca3 100%);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--gradient);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 16px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.auth-shell {
width: min(420px, 100%);
}
.container, .row, .col-md-5 {
width: 100%;
margin: 0;
padding: 0;
}
.login-card {
background: white;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.login-header {
background: var(--gradient);
color: white;
padding: 28px 24px;
text-align: center;
}
.login-header h2 {
font-size: 1.4rem;
line-height: 1.25;
margin-bottom: 8px;
}
.login-body {
padding: 24px 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #283247;
}
.input-group {
display: flex;
align-items: center;
border: 2px solid #e9ecef;
border-radius: 10px;
overflow: hidden;
background: #fff;
transition: all 0.3s;
}
.input-group:focus-within {
border-color: var(--primary);
box-shadow: 0 0 0 0.25rem rgba(67, 97, 238, 0.25);
}
.input-group-text {
border: none;
background: #f8f9ff;
padding: 0 12px;
color: #657089;
display: inline-flex;
align-items: center;
justify-content: center;
}
.form-control {
border: none;
border-radius: 0;
padding: 12px 15px;
transition: all 0.3s;
flex: 1;
min-width: 0;
}
.form-control:focus {
outline: none;
box-shadow: none;
}
.btn-primary {
background: var(--gradient);
border: none;
border-radius: 10px;
padding: 12px;
font-weight: 600;
transition: all 0.3s;
color: white;
width: 100%;
cursor: pointer;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(67, 97, 238, 0.3);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
}
.alert {
border-radius: 10px;
}
.text-center {
text-align: center;
}
.mb-0 {
margin-bottom: 0;
}
.mb-3 {
margin-bottom: 1rem;
}
.mb-4 {
margin-bottom: 1.5rem;
}
.w-100 {
width: 100%;
}
.me-2 {
margin-right: 0.5rem;
}
a {
color: var(--primary);
}
.text-decoration-none {
text-decoration: none;
}
@media (min-width: 992px) {
body {
padding: 24px;
}
.auth-shell {
width: 420px;
}
}
</style>
</head>
<body>
<div class="auth-shell">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-5">
<div class="login-card">
<div class="login-header">
<i class="fas fa-university fa-3x mb-3"></i>
<h2>Добро пожаловать в Банк "Школьный"</h2>
<p class="mb-0">__AUTH_SUBTITLE__</p>
</div>
<div class="login-body">
__AUTH_FORM__
__AUTH_ALERT__
__SECURITY_NOTICE__
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
'''
return (
page.replace("__AUTH_SUBTITLE__", subtitle)
.replace("__AUTH_FORM__", auth_form_html)
.replace("__AUTH_ALERT__", alert_html)
.replace("__SECURITY_NOTICE__", security_notice_html)
)
def render_register():
return '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Банк "Школьный" | Регистрация</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
:root {
--primary: #4361ee;
--secondary: #3a0ca3;
--gradient: linear-gradient(135deg, #4361ee 0%, #3a0ca3 100%);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--gradient);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 16px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.auth-shell {
width: min(460px, 100%);
}
.container, .row, .col-md-6 {
width: 100%;
margin: 0;
padding: 0;
}
.register-card {
background: white;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.register-header {
background: var(--gradient);
color: white;
padding: 28px 24px;
text-align: center;
}
.register-header h2 {
font-size: 1.3rem;
line-height: 1.25;
margin-bottom: 8px;
}
.register-body {
padding: 24px 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #283247;
}
.form-control {
border: 2px solid #e9ecef;
border-radius: 10px;
padding: 12px 15px;
transition: all 0.3s;
width: 100%;
}
.form-control:focus {
border-color: var(--primary);
outline: none;
box-shadow: 0 0 0 0.25rem rgba(67, 97, 238, 0.25);
}
.btn-primary {
background: var(--gradient);
border: none;
border-radius: 10px;
padding: 12px;
font-weight: 600;
transition: all 0.3s;
color: white;
width: 100%;
cursor: pointer;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(67, 97, 238, 0.3);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
}
.text-center {
text-align: center;
}
.mb-0 {
margin-bottom: 0;
}
.mb-3 {
margin-bottom: 1rem;
}
.mb-4 {
margin-bottom: 1.5rem;
}
.w-100 {
width: 100%;
}
.me-2 {
margin-right: 0.5rem;
}
a {
color: var(--primary);
}
.text-decoration-none {
text-decoration: none;
}
@media (min-width: 992px) {
body {
padding: 24px;
}
.auth-shell {
width: 460px;
}
}
</style>
</head>
<body>
<div class="auth-shell">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="register-card">
<div class="register-header">
<i class="fas fa-user-plus fa-3x mb-3"></i>
<h2>Регистрация аккаунта в банке "Школьный"</h2>
<p class="mb-0">Создайте свой банковский аккаунт</p>
</div>
<div class="register-body">
<form method="post">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Логин</label>
<input type="text" name="username" class="form-control" placeholder="Придумайте логин" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Пароль</label>
<input type="password" name="password" class="form-control" placeholder="Придумайте пароль" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Имя</label>
<input type="text" name="first_name" class="form-control" placeholder="Ваше имя" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Фамилия</label>
<input type="text" name="last_name" class="form-control" placeholder="Ваша фамилия" required>
</div>
</div>
<div class="mb-4">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-control" placeholder="email@example.com" required>
</div>
<div class="alert alert-warning small">
<strong>Важно:</strong> Мы не гарантируем полную безопасность сервиса. Все пароли хранятся в открытом виде, в базе данных, всем этим заведует один человек, поэтому я не гарантирую полную безопасность. Если понадобится восстановление, пишите на почту kompania.bank@gmail.com.
</div>
<button type="submit" class="btn btn-primary w-100 mb-3">
<i class="fas fa-user-plus me-2"></i>Создать аккаунт
</button>
<div class="text-center">
<p class="mb-0">Уже есть аккаунт? <a href="/" class="text-decoration-none">Войти</a></p>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
'''
def render_dashboard(user_data, history_data, shop_data=None, crypto_market=None, crypto_flash=None, active_tab="services"):
"""Генерация HTML личного кабинета"""
current_role = get_user_role(user_data)
can_take_credit = current_role in CREDIT_ROLES
can_trade_crypto = current_role in CRYPTO_ROLES
allowed_tabs = {"services", "shop", "history", "api"}
if can_trade_crypto:
allowed_tabs.add("crypto")
if active_tab not in allowed_tabs:
active_tab = "services"
if not isinstance(crypto_market, dict):
crypto_market = load_crypto_market()
crypto_market, decayed = apply_serkripto_idle_decay(crypto_market)
if decayed:
save_crypto_market(crypto_market)
ws_emit_crypto_market_event(crypto_market, "idle_decay")
crypto_price = round(float(crypto_market.get("price", 0.0)), 4)
crypto_user_amount = get_user_serkripto_amount(user_data)
crypto_portfolio_value = round(crypto_user_amount * crypto_price, 2)
crypto_stats = crypto_market.get("stats", {})
if not isinstance(crypto_stats, dict):
crypto_stats = {}
crypto_buy_ops = int(crypto_stats.get("buy_ops", 0))
crypto_sell_ops = int(crypto_stats.get("sell_ops", 0))
crypto_buy_volume = round(float(crypto_stats.get("buy_volume", 0.0)), 6)
crypto_sell_volume = round(float(crypto_stats.get("sell_volume", 0.0)), 6)
decay_rate_pct = round(float(crypto_market.get("idle_decay_rate", 0.0)) * 100, 3)
decay_interval = int(crypto_market.get("idle_decay_interval_sec", 30))
net_flow = round(crypto_buy_volume - crypto_sell_volume, 6)
max_buy_amount = max(0, int(user_data.get("balance", 0))) if isinstance(user_data, dict) else 0
max_sell_quantity = max(0.0, crypto_user_amount)
crypto_flash_html = ""
if isinstance(crypto_flash, dict) and crypto_flash.get("text"):
flash_class = crypto_flash.get("class", "info")
flash_text = str(crypto_flash.get("text"))
crypto_flash_html = f'<div class="alert alert-{flash_class} mb-3">{flash_text}</div>'
# Форматирование времени истории
for h in history_data:
h['formatted_time'] = datetime.fromtimestamp(h['time']).strftime('%d.%m.%Y %H:%M')
# HTML для истории
history_html = ""
for h in history_data[-10:][::-1]:
icon = "fa-history"
color = "text-primary"
if "deposit" in h['action']:
icon = "fa-piggy-bank"
color = "text-info"
elif "credit" in h['action']:
icon = "fa-credit-card"
color = "text-warning"
elif "transfer" in h['action']:
icon = "fa-exchange-alt"
color = "text-primary"
elif "card" in h['action']:
icon = "fa-credit-card"
color = "text-success"
elif "api" in h['action']:
icon = "fa-key"
color = "text-secondary"
elif "shop" in h['action'] or "item" in h['action'] or "advertisement" in h['action']:
icon = "fa-shopping-cart"
color = "text-success"
elif "payment" in h['action']:
icon = "fa-credit-card"
color = "text-success"
history_html += f'''
<tr>
<td><i class="fas {icon} {color} me-2"></i>{h['formatted_time']}</td>
<td><span class="badge bg-light text-dark">{h['user']}</span></td>
<td>{h['action']}</td>
<td class="fw-bold">{h['amount']:,} ₽</td>
<td>{h.get('target', '-')}</td>
</tr>
'''
# HTML для API ключа
api_key_html = ""
if user_data.get('api_key'):
api_key_html = f'''
<div class="alert alert-success">
<h6><i class="fas fa-key me-2"></i>Ваш API ключ</h6>
<div class="input-group">
<input type="text" id="apiKey" class="form-control" value="{user_data['api_key']}" readonly>
<button class="btn btn-outline-secondary" type="button" onclick="copyApiKey()">
<i class="fas fa-copy"></i>
</button>
</div>
<small class="text-muted">Используйте этот ключ для доступа к API</small>
</div>
'''
else:
api_key_html = '''
<div class="alert alert-warning">
<h6><i class="fas fa-key me-2"></i>API ключ не создан</h6>
<form action="/generate_api_key" method="post">
<button type="submit" class="btn btn-primary btn-sm">
<i class="fas fa-plus me-1"></i>Создать API ключ
</button>
</form>
</div>
'''
# HTML для магазина
shop_tab_content = ""
if shop_data:
# Товары
shop_items_html = ""
available_items = [item for item in shop_data["items"] if item["status"] == "available"]
if available_items:
for item in available_items[-10:][::-1]: # Последние 10 товаров
created_date = datetime.fromtimestamp(item["created"]).strftime('%d.%m.%Y')
shop_items_html += f'''
<div class="col-md-6 col-lg-4">
<div class="card h-100 shop-item-card">
<div class="card-body">
<h5 class="card-title">{item["title"]}</h5>
<p class="card-text">{item["description"]}</p>
<p class="card-text">
<small class="text-muted">Продавец: {item["seller"]}</small><br>
<small class="text-muted">Категория: {item["category"]}</small><br>
<small class="text-muted">Дата: {created_date}</small>
</p>
<div class="d-flex justify-content-between align-items-center">
<span class="h4 text-success">{item["price"]:,} ₽</span>
<form action="/buy_item/{item['id']}" method="post">
<button type="submit" class="btn btn-primary">
<i class="fas fa-shopping-cart me-1"></i> Купить
</button>
</form>
</div>
</div>
</div>
</div>
'''
else:
shop_items_html = '''
<div class="col-12">
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
В магазине пока нет товаров. Будьте первым, кто добавит товар!
</div>
</div>
'''
# Объявления
ads_html = ""
active_ads = [ad for ad in shop_data["ads"] if ad["status"] == "active"]
if active_ads:
for ad in active_ads[-10:][::-1]: # Последние 10 объявлений
created_date = datetime.fromtimestamp(ad["created"]).strftime('%d.%m.%Y')
ads_html += f'''
<div class="col-md-6">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">{ad["title"]}</h5>
<p class="card-text">{ad["content"]}</p>
<p class="card-text">
<small class="text-muted">Автор: {ad["author"]}</small><br>
<small class="text-muted">Контакты: {ad["contact_info"]}</small><br>
<small class="text-muted">Дата: {created_date}</small>
</p>
</div>
</div>
</div>
'''
shop_tab_content = f'''
<div class="tab-pane fade" id="shop">
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between mb-3">
<h4><i class="fas fa-store me-2"></i>Магазин</h4>
<div>
<button class="btn btn-success me-2" data-bs-toggle="modal" data-bs-target="#addItemModal">
<i class="fas fa-plus me-1"></i>Добавить товар
</button>
<button class="btn btn-info" data-bs-toggle="modal" data-bs-target="#addAdModal">
<i class="fas fa-bullhorn me-1"></i>Добавить объявление
</button>
</div>
</div>
</div>
</div>
<h5><i class="fas fa-shopping-bag me-2"></i>Товары в магазине</h5>
<div class="row g-4 mb-4">
{shop_items_html}
</div>
<h5><i class="fas fa-bullhorn me-2"></i>Объявления</h5>
<div class="row g-4">
{ads_html}
</div>
</div>
'''
crypto_tab_content = f'''
<div class="tab-pane fade" id="crypto">
<div class="row g-4">
<div class="col-lg-7">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h4 class="mb-3"><i class="fas fa-coins text-warning me-2"></i>{SERKRIPTO_NAME} ({crypto_market.get("symbol", SERKRIPTO_SYMBOL)})</h4>
{crypto_flash_html}
<div class="crypto-price-box mb-3">
<div class="small text-muted">Текущая цена</div>
<div class="display-6 fw-bold">{crypto_price:,.4f} ₽</div>
<div class="small text-muted mt-1">
Если монету покупают, цена растет. Если покупок нет и идут продажи, цена снижается.
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<div class="crypto-stat">
<div class="text-muted small">У вас монет</div>
<div class="h5 mb-0">{crypto_user_amount:,.6f} {crypto_market.get("symbol", SERKRIPTO_SYMBOL)}</div>
</div>
</div>
<div class="col-md-6">
<div class="crypto-stat">
<div class="text-muted small">Оценка портфеля</div>
<div class="h5 mb-0">{crypto_portfolio_value:,.2f} ₽</div>
</div>
</div>
</div>
<div class="small text-muted">
Автоспад цены без покупок: {decay_rate_pct}% каждые {decay_interval} сек.
</div>
</div>
</div>
</div>
<div class="col-lg-5">
<div class="card border-0 shadow-sm mb-3">
<div class="card-body">
<h5 class="mb-3"><i class="fas fa-arrow-up text-success me-2"></i>Купить {SERKRIPTO_NAME}</h5>
<form action="/crypto/buy" method="post">
<label class="form-label">Сумма покупки (₽)</label>
<div class="input-group mb-2">
<input type="number" id="cryptoBuyAmount" name="amount" class="form-control" min="1" step="1" max="{max_buy_amount}" data-max-value="{max_buy_amount}" required placeholder="Например: 1000">
<button type="button" class="btn btn-outline-secondary" id="cryptoBuyMaxBtn">Максимум</button>
</div>
<small class="text-muted d-block mb-3">Максимум для покупки: {max_buy_amount:,} ₽</small>
<button type="submit" class="btn btn-success w-100">
<i class="fas fa-cart-plus me-1"></i>Купить
</button>
</form>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body">
<h5 class="mb-3"><i class="fas fa-arrow-down text-danger me-2"></i>Продать {SERKRIPTO_NAME}</h5>
<form action="/crypto/sell" method="post">
<label class="form-label">Количество монет</label>
<div class="input-group mb-2">
<input type="number" id="cryptoSellQuantity" name="quantity" class="form-control" min="0.000001" step="0.000001" max="{max_sell_quantity:.6f}" data-max-value="{max_sell_quantity:.6f}" required placeholder="Например: 0.250000">
<button type="button" class="btn btn-outline-secondary" id="cryptoSellMaxBtn">Максимум</button>
</div>
<small class="text-muted d-block mb-3">Максимум для продажи: {max_sell_quantity:,.6f} {crypto_market.get("symbol", SERKRIPTO_SYMBOL)}</small>
<button type="submit" class="btn btn-danger w-100">
<i class="fas fa-money-bill-wave me-1"></i>Продать
</button>
</form>
</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm mt-4">
<div class="card-body">
<h5 class="mb-3"><i class="fas fa-chart-line me-2"></i>Статистика рынка</h5>
<div class="row g-3">
<div class="col-md-3">
<div class="crypto-stat"><div class="text-muted small">Покупок</div><div class="h6 mb-0">{crypto_buy_ops}</div></div>
</div>
<div class="col-md-3">
<div class="crypto-stat"><div class="text-muted small">Продаж</div><div class="h6 mb-0">{crypto_sell_ops}</div></div>
</div>
<div class="col-md-3">
<div class="crypto-stat"><div class="text-muted small">Куплено монет</div><div class="h6 mb-0">{crypto_buy_volume:,.6f}</div></div>
</div>
<div class="col-md-3">
<div class="crypto-stat"><div class="text-muted small">Чистый поток</div><div class="h6 mb-0 {'text-success' if net_flow >= 0 else 'text-danger'}">{net_flow:,.6f}</div></div>
</div>
</div>
</div>
</div>
</div>
'''
crypto_tab_block = crypto_tab_content if can_trade_crypto else ""
crypto_nav_tab = ""
crypto_service_card = ""
if can_trade_crypto:
crypto_nav_tab = '''
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#crypto">
<i class="fas fa-coins me-1"></i>SerKripto
</a>
</li>
'''
crypto_service_card = '''
<div class="service-card">
<h5><i class="fas fa-coins text-warning me-2"></i>SerKripto</h5>
<p class="text-muted">Покупки поднимают курс, продажи и отсутствие покупок снижают цену.</p>
</div>
'''
credit_action_btn = ""
if can_take_credit:
credit_action_btn = '''
<button class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#creditModal">
<i class="fas fa-money-bill-wave me-1"></i>Взять кредит
</button>
'''
# HTML для админ-панели
admin_tab = ""
if current_role == ROLE_ADMIN:
admin_tab = '''
<li class="nav-item">
<a class="nav-link" href="/admin">
<i class="fas fa-cogs me-2"></i>Админ-панель
</a>
</li>
'''
teacher_tab = ""
if current_role in [ROLE_TEACHER, ROLE_ADMIN]:
teacher_tab = '''
<li class="nav-item">
<a class="nav-link" href="/teacher">
<i class="fas fa-school me-2"></i>Учительская панель
</a>
</li>
'''
student_tab = ""
student_panel_btn = ""
if current_role == ROLE_STUDENT:
student_tab = '''
<li class="nav-item">
<a class="nav-link" href="/student_panel">
<i class="fas fa-user-graduate me-2"></i>Панель ученика
</a>
</li>
'''
student_panel_btn = '''
<a href="/student_panel" class="btn btn-info">
<i class="fas fa-chart-line me-1"></i>Моя успеваемость
</a>
'''
parent_tab = ""
parent_panel_btn = ""
if current_role == ROLE_PARENT:
parent_tab = '''
<li class="nav-item">
<a class="nav-link" href="/parent_panel">
<i class="fas fa-people-roof me-2"></i>Родительская панель
</a>
</li>
'''
parent_panel_btn = '''
<a href="/parent_panel" class="btn btn-secondary">
<i class="fas fa-children me-1"></i>Мои дети
</a>
'''
manager_tab = ""
manager_panel_btn = ""
if current_role in [ROLE_MANAGER, ROLE_ADMIN]:
manager_tab = '''
<li class="nav-item">
<a class="nav-link" href="/manager_panel">
<i class="fas fa-briefcase me-2"></i>Панель менеджера
</a>
</li>
'''
manager_panel_btn = '''
<a href="/manager_panel" class="btn btn-dark">
<i class="fas fa-chart-pie me-1"></i>Аналитика
</a>
'''
return f'''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Банк "Школьный" | Личный кабинет</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
:root {{
--primary: #4361ee;
--secondary: #3a0ca3;
--gradient: linear-gradient(135deg, #4361ee 0%, #3a0ca3 100%);
}}
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f7fb;
}}
.navbar-custom {{
background: var(--gradient) !important;
box-shadow: 0 4px 20px rgba(67, 97, 238, 0.3);
}}
.sidebar {{
background: white;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
padding: 20px;
height: fit-content;
}}
.main-content {{
background: white;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
padding: 25px;
}}
.balance-card {{
background: var(--gradient);
color: white;
border-radius: 15px;
padding: 20px;
margin-bottom: 20px;
}}
.stat-card {{
background: white;
border-radius: 15px;
padding: 15px;
box-shadow: 0 3px 10px rgba(0,0,0,0.05);
border-left: 4px solid var(--primary);
margin-bottom: 15px;
}}
.nav-tabs .nav-link {{
border: none;
border-radius: 10px;
padding: 10px 15px;
margin: 0 5px;
color: #666;
font-weight: 500;
}}
.nav-tabs .nav-link.active {{
background: var(--gradient);
color: white;
}}
.table-custom {{
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 3px 10px rgba(0,0,0,0.05);
}}
.table-custom thead {{
background: var(--gradient);
color: white;
}}
.btn-primary {{
background: var(--gradient);
border: none;
border-radius: 10px;
padding: 8px 15px;
font-weight: 600;
}}
.btn-primary:hover {{
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(67, 97, 238, 0.3);
}}
.modal-header {{
background: var(--gradient);
color: white;
}}
.services-grid {{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
}}
.service-card {{
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 3px 10px rgba(0,0,0,0.05);
border-left: 4px solid var(--primary);
transition: all 0.3s;
}}
.service-card:hover {{
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}}
.api-docs {{
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
margin-top: 20px;
}}
.code-block {{
background: #1e1e1e;
color: #d4d4d4;
padding: 15px;
border-radius: 5px;
font-family: "Courier New", monospace;
overflow-x: auto;
margin: 10px 0;
}}
.shop-item-card {{
transition: all 0.3s;
}}
.shop-item-card:hover {{
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}}
.crypto-price-box {{
background: linear-gradient(135deg, #f7fbff 0%, #eef3ff 100%);
border: 1px solid #d8e4ff;
border-radius: 12px;
padding: 16px;
}}
.crypto-stat {{
background: #f8faff;
border: 1px solid #e5edff;
border-radius: 10px;
padding: 12px;
height: 100%;
}}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-custom navbar-dark">
<div class="container">
<a class="navbar-brand d-flex align-items-center" href="/dashboard">
<i class="fas fa-university fa-2x me-2"></i>
<div>
<h4 class="mb-0">Банк "Школьный"</h4>
<small class="opacity-75">Финансовая система в школьных условиях</small>
</div>
</a>
<div class="d-flex align-items-center">
<span class="badge bg-light text-dark me-3">
<i class="fas fa-user-circle me-1"></i>
{session['user']} ({get_user_role(user_data)})
</span>
<a href="/logout" class="btn btn-outline-light btn-sm">
<i class="fas fa-sign-out-alt me-1"></i>Выход
</a>
</div>
</div>
</nav>
<div class="container mt-4 mb-5">
<div class="row g-4">
<div class="col-lg-3">
<div class="sidebar">
<h5 class="mb-3"><i class="fas fa-chart-line me-2"></i>Финансы</h5>
<div class="balance-card">
<h6 class="opacity-75">Текущий баланс</h6>
<h2 class="{'text-success' if user_data['balance'] >= 0 else 'text-danger'}">
{user_data['balance']:,}
</h2>
<small>Доступные средства</small>
</div>
<div class="stat-card">
<div class="d-flex justify-content-between">
<div>
<h6>Вклад</h6>
<h5 class="text-success">{user_data['deposit']:,} ₽</h5>
</div>
<i class="fas fa-piggy-bank fa-2x text-success"></i>
</div>
<small class="text-muted">+1% ежедневно</small>
</div>
<div class="stat-card">
<div class="d-flex justify-content-between">
<div>
<h6>Кредит</h6>
<h5 class="text-danger">{user_data['credit']:,} ₽</h5>
</div>
<i class="fas fa-credit-card fa-2x text-danger"></i>
</div>
<small class="text-muted">+5% ежедневно</small>
</div>
<div class="mt-3">
<h6><i class="fas fa-wallet me-2"></i>Операции</h6>
<div class="d-grid gap-2 mt-2">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#depositModal">
<i class="fas fa-plus-circle me-1"></i>Пополнить вклад
</button>
{credit_action_btn}
<button class="btn btn-outline-success" data-bs-toggle="modal" data-bs-target="#transferModal">
<i class="fas fa-exchange-alt me-1"></i>Перевод
</button>
<a href="/payment_page" class="btn btn-warning">
<i class="fas fa-credit-card me-1"></i>Оплата
</a>
{student_panel_btn}
{parent_panel_btn}
{manager_panel_btn}
</div>
</div>
<div class="mt-4">
{api_key_html}
</div>
</div>
</div>
<div class="col-lg-9">
<div class="main-content">
<ul class="nav nav-tabs mb-4">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#services">
<i class="fas fa-concierge-bell me-1"></i>Услуги
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#shop">
<i class="fas fa-store me-1"></i>Магазин
</a>
</li>
{crypto_nav_tab}
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#history">
<i class="fas fa-history me-1"></i>История
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#api">
<i class="fas fa-code me-1"></i>API
</a>
</li>
{student_tab}
{parent_tab}
{manager_tab}
{teacher_tab}
{admin_tab}
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="services">
<div class="services-grid">
<div class="service-card">
<h5><i class="fas fa-piggy-bank text-primary me-2"></i>Вклады</h5>
<p class="text-muted">Откройте вклад под 1% ежедневно и получайте пассивный доход.</p>
</div>
<div class="service-card">
<h5><i class="fas fa-credit-card text-success me-2"></i>Кредиты</h5>
<p class="text-muted">Получите кредит на любые цели. Ставка 5% ежедневно.</p>
</div>
<div class="service-card">
<h5><i class="fas fa-exchange-alt text-info me-2"></i>Переводы</h5>
<p class="text-muted">Быстрые переводы между клиентами банка без комиссий.</p>
</div>
<div class="service-card">
<h5><i class="fas fa-store text-danger me-2"></i>Магазин</h5>
<p class="text-muted">Покупайте и продавайте товары, размещайте объявления.</p>
</div>
<div class="service-card">
<h5><i class="fas fa-credit-card text-secondary me-2"></i>Платежи</h5>
<p class="text-muted">Принимайте платежи от клиентов через API или ссылки.</p>
</div>
{crypto_service_card}
<div class="service-card">
<h5><i class="fas fa-code text-dark me-2"></i>API Доступ</h5>
<p class="text-muted">Интегрируйте банковские услуги в ваши приложения через API.</p>
</div>
</div>
</div>
{shop_tab_content}
{crypto_tab_block}
<div class="tab-pane fade" id="history">
<div class="table-responsive">
<table class="table table-custom table-hover">
<thead>
<tr>
<th><i class="fas fa-clock"></i> Время</th>
<th><i class="fas fa-user"></i> Пользователь</th>
<th><i class="fas fa-tasks"></i> Операция</th>
<th><i class="fas fa-ruble-sign"></i> Сумма</th>
<th><i class="fas fa-user-friends"></i>Получатель</th>
</tr>
</thead>
<tbody>
{history_html}
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="api">
<div class="api-docs">
<h4><i class="fas fa-code me-2"></i>Документация API</h4>
<p>Используйте ваш API ключ для доступа к банковским операциям через REST API.</p>
<h5 class="mt-4">Базовый URL</h5>
<div class="code-block">
http://ваш-домен/api/v1/
</div>
<h5 class="mt-4">Аутентификация</h5>
<p>Добавьте заголовок с вашим API ключом:</p>
<div class="code-block">
X-API-Key: ваш_api_ключ
</div>
<p>Или передайте как параметр:</p>
<div class="code-block">
?api_key=ваш_api_ключ
</div>
<h5 class="mt-4">Доступные методы</h5>
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>Метод</th>
<th>Эндпоинт</th>
<th>Описание</th>
<th>Параметры</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="badge bg-info">GET</span></td>
<td>/api/v1/balance</td>
<td>Получить информацию о балансе</td>
<td>-</td>
</tr>
<tr>
<td><span class="badge bg-info">GET</span></td>
<td>/api/v1/history</td>
<td>Получить историю операций</td>
<td>limit (опционально)</td>
</tr>
<tr>
<td><span class="badge bg-success">POST</span></td>
<td>/api/v1/deposit</td>
<td>Открыть вклад</td>
<td>amount (int)</td>
</tr>
<tr>
<td><span class="badge bg-success">POST</span></td>
<td>/api/v1/credit</td>
<td>Взять кредит</td>
<td>amount (int)</td>
</tr>
<tr>
<td><span class="badge bg-success">POST</span></td>
<td>/api/v1/transfer</td>
<td>Сделать перевод</td>
<td>target (str), amount (int)</td>
</tr>
<tr>
<td><span class="badge bg-info">GET</span></td>
<td>/api/v1/shop/items</td>
<td>Получить список товаров</td>
<td>-</td>
</tr>
<tr>
<td><span class="badge bg-info">GET</span></td>
<td>/api/v1/shop/ads</td>
<td>Получить список объявлений</td>
<td>-</td>
</tr>
<tr>
<td><span class="badge bg-info">GET</span></td>
<td>/api/v1/crypto</td>
<td>Текущая цена и кошелек SerKripto</td>
<td>-</td>
</tr>
<tr>
<td><span class="badge bg-success">POST</span></td>
<td>/api/v1/crypto/buy</td>
<td>Купить SerKripto за рубли</td>
<td>amount (int)</td>
</tr>
<tr>
<td><span class="badge bg-success">POST</span></td>
<td>/api/v1/crypto/sell</td>
<td>Продать SerKripto</td>
<td>quantity (float)</td>
</tr>
<tr>
<td><span class="badge bg-success">POST</span></td>
<td>/api/v1/payment/create</td>
<td>Создать платеж</td>
<td>receiver, amount, description</td>
</tr>
</tbody>
</table>
</div>
<h5 class="mt-4">Примеры использования</h5>
<p>Python с requests:</p>
<div class="code-block">
import requests<br>
<br>
api_key = "ваш_api_ключ"<br>
base_url = "http://ваш-домен/api/v1"<br>
<br>
# Получить баланс<br>
headers = {{"X-API-Key": api_key}}<br>
response = requests.get(f"{{base_url}}/balance", headers=headers)<br>
print(response.json())<br>
<br>
# Сделать перевод<br>
data = {{"target": "получатель", "amount": 100}}<br>
response = requests.post(f"{{base_url}}/transfer", headers=headers, json=data)<br>
print(response.json())
</div>
<p>cURL:</p>
<div class="code-block">
# Получить баланс<br>
curl -H "X-API-Key: ваш_api_ключ" http://ваш-домен/api/v1/balance<br>
<br>
# Сделать перевод<br>
curl -X POST -H "X-API-Key: ваш_api_ключ" \\<br>
-H "Content-Type: application/json" \\<br>
-d '{{"target": "получатель", "amount": 100}}' \\<br>
http://ваш-домен/api/v1/transfer
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Модальные окна -->
<div class="modal fade" id="depositModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-piggy-bank me-2"></i>Вклад</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form action="/deposit" method="post">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Сумма вклада (₽)</label>
<input type="number" name="amount" class="form-control" min="1" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="submit" class="btn btn-primary">Подтвердить</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="creditModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-credit-card me-2"></i>Кредит</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form action="/credit" method="post">
<div class="modal-body">
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
Кредит выдается под 5% ежедневно
</div>
<div class="mb-3">
<label class="form-label">Сумма кредита (₽)</label>
<input type="number" name="amount" class="form-control" min="1" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="submit" class="btn btn-primary">Взять кредит</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="transferModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-exchange-alt me-2"></i>Перевод</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form action="/transfer" method="post">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Получатель</label>
<input type="text" name="target" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Сумма (₽)</label>
<input type="number" name="amount" class="form-control" min="1" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="submit" class="btn btn-primary">Отправить</button>
</div>
</form>
</div>
</div>
</div>
<!-- Модальные окна для магазина -->
<div class="modal fade" id="addItemModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-plus me-2"></i>Добавить товар</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form action="/create_shop_item" method="post">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Название товара</label>
<input type="text" name="title" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Описание</label>
<textarea name="description" class="form-control" rows="3" required></textarea>
</div>
<div class="mb-3">
<label class="form-label">Цена (₽)</label>
<input type="number" name="price" class="form-control" min="1" required>
</div>
<div class="mb-3">
<label class="form-label">Категория</label>
<select name="category" class="form-select">
<option value="electronics">Электроника</option>
<option value="clothing">Одежда</option>
<option value="books">Книги</option>
<option value="other">Другое</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="submit" class="btn btn-primary">Добавить товар</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="addAdModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-bullhorn me-2"></i>Добавить объявление</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form action="/create_advertisement" method="post">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Заголовок</label>
<input type="text" name="title" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Содержание</label>
<textarea name="content" class="form-control" rows="3" required></textarea>
</div>
<div class="mb-3">
<label class="form-label">Контактная информация</label>
<input type="text" name="contact_info" class="form-control" placeholder="Email или телефон" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="submit" class="btn btn-primary">Добавить объявление</button>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Автообновление каждые 60 секунд
setInterval(() => {{
window.location.reload();
}}, 60000);
// Восстановление активной вкладки
(() => {{
const preferredTab = "{active_tab}";
const hashTab = window.location.hash ? window.location.hash.replace("#", "") : "";
const tabToOpen = hashTab || preferredTab;
if (!tabToOpen || !window.bootstrap) return;
const trigger = document.querySelector(`a[data-bs-toggle="tab"][href="#${{tabToOpen}}"]`);
if (trigger) {{
new bootstrap.Tab(trigger).show();
}}
}})();
// Кнопки "Максимум" для покупки/продажи SerKripto
(() => {{
const bindMaxButton = (inputId, buttonId, emptyText) => {{
const input = document.getElementById(inputId);
const button = document.getElementById(buttonId);
if (!input || !button) return;
button.addEventListener("click", () => {{
const maxValue = parseFloat(input.getAttribute("data-max-value") || "0");
if (!Number.isFinite(maxValue) || maxValue <= 0) {{
alert(emptyText);
return;
}}
input.value = input.step && input.step.includes(".") ? maxValue.toFixed(6) : Math.floor(maxValue);
input.dispatchEvent(new Event("input", {{ bubbles: true }}));
}});
}};
bindMaxButton("cryptoBuyAmount", "cryptoBuyMaxBtn", "Недостаточно средств для покупки");
bindMaxButton("cryptoSellQuantity", "cryptoSellMaxBtn", "Нет монет для продажи");
}})();
// Валидация форм
document.querySelectorAll("form").forEach(form => {{
form.addEventListener("submit", function(e) {{
const inputs = this.querySelectorAll("input[type=\"number\"]");
inputs.forEach(input => {{
if (input.value && parseFloat(input.value) <= 0) {{
e.preventDefault();
alert("Пожалуйста, введите корректное значение");
}}
}});
}});
}});
// Анимация карт
// Копирование API ключа
function copyApiKey() {{
const apiKeyInput = document.getElementById("apiKey");
if (!apiKeyInput) return;
apiKeyInput.select();
apiKeyInput.setSelectionRange(0, 99999);
document.execCommand("copy");
alert("API ключ скопирован в буфер обмена!");
}}
</script>
</body>
</html>
'''
def render_admin_panel(users):
"""Генерация HTML админ-панели - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
users_html = ""
current_user = session.get('user')
valid_users_count = 0
total_balance = 0
total_deposit = 0
total_credit = 0
for username, user in users.items():
# Пропускаем текущего пользователя
if username == current_user:
continue
# Проверяем, что пользователь - словарь (корректные данные)
if not isinstance(user, dict):
continue
# Безопасно получаем данные пользователя
role = get_user_role(user)
balance = user.get('balance', 0)
deposit = user.get('deposit', 0)
credit = user.get('credit', 0)
email = user.get('email', 'нет')
# Обновляем статистику
valid_users_count += 1
total_balance += balance
total_deposit += deposit
total_credit += credit
# Определяем цвет бейджа роли
badge_color_map = {
ROLE_ADMIN: "danger",
ROLE_MANAGER: "warning",
ROLE_TEACHER: "info",
ROLE_PARENT: "primary",
ROLE_STUDENT: "secondary",
ROLE_CLIENT: "dark"
}
badge_color = badge_color_map.get(role, "dark")
users_html += f'''
<div class="col-md-6 col-lg-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">{username}</h5>
<p class="card-text">
<small>Роль: <span class="badge bg-{badge_color}">{role}</span></small><br>
<small>Баланс: <strong>{balance:,} ₽</strong></small><br>
<small>Вклад: <strong>{deposit:,} ₽</strong></small><br>
<small>Кредит: <strong>{credit:,} ₽</strong></small><br>
<small>Email: {email}</small>
</p>
<form action="/admin/change_role" method="post" class="mb-2">
<input type="hidden" name="username" value="{username}">
<select name="role" class="form-select form-select-sm mb-2">
<option value="{ROLE_CLIENT}" {'selected' if role == ROLE_CLIENT else ''}>Клиент</option>
<option value="{ROLE_STUDENT}" {'selected' if role == ROLE_STUDENT else ''}>Ученик</option>
<option value="{ROLE_PARENT}" {'selected' if role == ROLE_PARENT else ''}>Родитель</option>
<option value="{ROLE_TEACHER}" {'selected' if role == ROLE_TEACHER else ''}>Учитель</option>
<option value="{ROLE_MANAGER}" {'selected' if role == ROLE_MANAGER else ''}>Менеджер</option>
<option value="{ROLE_ADMIN}" {'selected' if role == ROLE_ADMIN else ''}>Админ</option>
</select>
<button type="submit" class="btn btn-warning btn-sm w-100">Изменить роль</button>
</form>
<form action="/admin/add_money" method="post">
<input type="hidden" name="username" value="{username}">
<div class="input-group input-group-sm mb-2">
<input type="number" name="amount" class="form-control" placeholder="Сумма" min="1" required>
<button class="btn btn-success" type="submit">+</button>
</div>
</form>
</div>
</div>
</div>
'''
return f'''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Админ-панель</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
:root {{
--primary: #4361ee;
--gradient: linear-gradient(135deg, #4361ee 0%, #3a0ca3 100%);
}}
body {{
background: #f5f7fb;
padding: 20px;
}}
.admin-header {{
background: var(--gradient);
color: white;
border-radius: 15px;
padding: 20px;
margin-bottom: 20px;
}}
.card {{
border-radius: 12px;
box-shadow: 0 3px 10px rgba(0,0,0,0.05);
}}
.stat-card {{
background: white;
border-radius: 12px;
padding: 15px;
box-shadow: 0 3px 10px rgba(0,0,0,0.05);
border-left: 4px solid var(--primary);
margin-bottom: 15px;
}}
</style>
</head>
<body>
<div class="container">
<div class="admin-header">
<div class="d-flex justify-content-between align-items-center">
<div>
<h2><i class="fas fa-cogs me-2"></i>Админ-панель</h2>
<p class="mb-0">Управление пользователями</p>
</div>
<a href="/dashboard" class="btn btn-light">
<i class="fas fa-arrow-left me-1"></i>Назад
</a>
</div>
</div>
<div class="row g-4">
{users_html if users_html else '''
<div class="col-12">
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
Нет других пользователей для управления.
</div>
</div>
'''}
</div>
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<h5 class="card-title"><i class="fas fa-chart-bar me-2"></i>Статистика системы</h5>
<div class="row">
<div class="col-md-3">
<div class="stat-card">
<h6>Всего пользователей</h6>
<h4>{valid_users_count + 1}</h4>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<h6>Общий баланс</h6>
<h4 class="text-success">{total_balance:,} ₽</h4>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<h6>Общие вклады</h6>
<h4 class="text-info">{total_deposit:,} ₽</h4>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<h6>Общие кредиты</h6>
<h4 class="text-danger">{total_credit:,} ₽</h4>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Валидация форм админ-панели
document.querySelectorAll("form").forEach(form => {{
form.addEventListener("submit", function(e) {{
const amountInput = this.querySelector('input[type="number"]');
if (amountInput && amountInput.value) {{
if (parseInt(amountInput.value) <= 0) {{
e.preventDefault();
alert("Сумма должна быть положительной!");
}}
}}
}});
}});
</script>
</body>
</html>
'''
# ---------- СТРАНИЦА ОПЛАТЫ ----------
def render_payment_page(created_payment=None, error_message=""):
"""Страница оплаты"""
users = load_users()
current_user = session.get("user")
user_data = users.get(current_user, {}) if isinstance(users.get(current_user), dict) else {}
api_key = user_data.get("api_key")
payment_rows = ""
payments = load_payments()
user_payments = []
for payment in payments:
if payment.get("created_by") == current_user or payment.get("payer") == current_user or payment.get("receiver") == current_user:
user_payments.append(payment)
user_payments.sort(key=lambda x: x.get("created", 0), reverse=True)
for payment in user_payments[:20]:
status = payment.get("status", "pending")
status_class = "warning"
if status == "completed":
status_class = "success"
elif status == "failed":
status_class = "danger"
elif status == "cancelled":
status_class = "secondary"
payer_label = payment.get("payer", "-")
if payment.get("allow_any_payer") and status == "pending":
payer_label = "любой"
payment_rows += f"""
<tr>
<td><small>{payment.get('id', '-')}</small></td>
<td><strong>{payment.get('amount', 0)} ₽</strong></td>
<td>{payment.get('receiver', '-')}</td>
<td>{payer_label}</td>
<td><span class="badge bg-{status_class}">{status}</span></td>
<td><small>{datetime.fromtimestamp(payment.get('created', 0)).strftime('%Y-%m-%d %H:%M:%S')}</small></td>
<td><a class="btn btn-sm btn-outline-primary" href="/pay/{payment.get('id', '')}" target="_blank">Открыть</a></td>
</tr>
"""
if not payment_rows:
payment_rows = "<tr><td colspan='7' class='text-center text-muted'>Пока нет платежей</td></tr>"
created_block = ""
if created_payment and isinstance(created_payment, dict):
payment_url = f"{request.host_url.rstrip('/')}/pay/{created_payment.get('id')}"
created_block = f"""
<div class="alert alert-success mt-3">
<h6><i class="fas fa-check-circle me-2"></i>Ссылка создана</h6>
<p class="mb-1">ID платежа: <strong>{created_payment.get('id')}</strong></p>
<div class="payment-link" id="generatedPaymentLink">{payment_url}</div>
<button class="btn btn-sm btn-outline-success" onclick="copyPaymentLink()">
<i class="fas fa-copy me-1"></i>Скопировать ссылку
</button>
</div>
"""
api_key_hint = ""
if api_key:
api_key_hint = f"""
<div class="alert alert-success">
<small><strong>Ваш API ключ:</strong> <code>{api_key}</code></small>
</div>
"""
else:
api_key_hint = """
<div class="alert alert-warning">
<small>API ключ не создан. Сначала сгенерируйте ключ в личном кабинете.</small>
</div>
"""
error_block = ""
if error_message:
error_block = f'<div class="alert alert-danger mb-3">{error_message}</div>'
return f'''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Оплата | Банковская система</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
:root {{
--primary: #4361ee;
--secondary: #3a0ca3;
--gradient: linear-gradient(135deg, #4361ee 0%, #3a0ca3 100%);
}}
body {{
background: #f5f7fb;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}}
.payment-card {{
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
overflow: hidden;
}}
.payment-header {{
background: var(--gradient);
color: white;
padding: 25px;
text-align: center;
}}
.payment-body {{
padding: 30px;
}}
.form-control {{
border: 2px solid #e9ecef;
border-radius: 10px;
padding: 12px 15px;
transition: all 0.3s;
}}
.form-control:focus {{
border-color: var(--primary);
box-shadow: 0 0 0 0.25rem rgba(67, 97, 238, 0.25);
}}
.btn-primary {{
background: var(--gradient);
border: none;
border-radius: 10px;
padding: 12px;
font-weight: 600;
transition: all 0.3s;
}}
.btn-primary:hover {{
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(67, 97, 238, 0.3);
}}
.api-example {{
background: #f8f9fa;
border-radius: 10px;
padding: 15px;
font-family: 'Courier New', monospace;
font-size: 14px;
margin: 10px 0;
}}
.payment-link {{
background: #e9ecef;
border-radius: 8px;
padding: 10px;
word-break: break-all;
font-family: 'Courier New', monospace;
margin: 10px 0;
}}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark" style="background: var(--gradient);">
<div class="container">
<a class="navbar-brand" href="/dashboard">
<i class="fas fa-university me-2"></i>
Банк | Оплата
</a>
<div>
<a href="/dashboard" class="btn btn-outline-light btn-sm">
<i class="fas fa-arrow-left me-1"></i> Назад
</a>
</div>
</div>
</nav>
<div class="container mt-4 mb-5">
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="payment-card">
<div class="payment-header">
<h3><i class="fas fa-credit-card me-2"></i>Платежная система</h3>
<p class="mb-0">Создавайте ссылки и принимайте оплату</p>
</div>
<div class="payment-body">
{error_block}
{created_block}
<div class="row">
<div class="col-md-6">
<h4><i class="fas fa-link me-2"></i>Создать ссылку для оплаты</h4>
<form action="/create_payment_link" method="post" id="paymentForm">
<div class="mb-3">
<label class="form-label">Получатель (логин)</label>
<input type="text" name="receiver" class="form-control" required value="{current_user or ''}">
</div>
<div class="mb-3">
<label class="form-label">Плательщик (логин, необязательно)</label>
<input type="text" name="payer" class="form-control" placeholder="Пусто = оплатить сможет любой пользователь">
</div>
<div class="mb-3">
<label class="form-label">Сумма (₽)</label>
<input type="number" name="amount" class="form-control" min="1" required placeholder="Введите сумму">
</div>
<div class="mb-3">
<label class="form-label">Описание платежа</label>
<input type="text" name="description" class="form-control" placeholder="Оплата за товар/услугу">
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-link me-2"></i>Создать ссылку
</button>
</form>
</div>
<div class="col-md-6">
<h4><i class="fas fa-code me-2"></i>API для оплаты</h4>
{api_key_hint}
<div class="api-example">
POST /api/v1/payment/create<br>
Header: X-API-Key: ваш_ключ<br>
Body: {{"receiver":"{current_user or 'username'}","amount":100,"description":"Оплата","allow_any_payer":true}}
</div>
<div class="api-example">
GET /api/v1/payment/status/payment_id<br>
Header: X-API-Key: ваш_ключ
</div>
</div>
</div>
<div class="mt-5">
<h4><i class="fas fa-history me-2"></i>Последние платежи</h4>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>ID</th>
<th>Сумма</th>
<th>Получатель</th>
<th>Плательщик</th>
<th>Статус</th>
<th>Дата</th>
<th>Ссылка</th>
</tr>
</thead>
<tbody>
{payment_rows}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
function copyPaymentLink() {{
const linkElement = document.getElementById('generatedPaymentLink');
if (!linkElement) return;
const text = linkElement.textContent.trim();
if (navigator.clipboard) {{
navigator.clipboard.writeText(text);
}}
}}
</script>
</body>
</html>
'''
def render_pay_page(payment_id):
"""Страница оплаты для клиента"""
payments = load_payments()
error_code = request.args.get("error", "").strip()
error_map = {
"invalid_credentials": "Неверный логин или пароль",
"payment_not_found": "Платеж не найден",
"wrong_payer": "Вы не являетесь плательщиком по этому платежу",
"insufficient_funds": "Недостаточно средств на счете плательщика",
"payer_not_found": "Плательщик не найден",
"receiver_not_found": "Получатель не найден",
"role_denied": "Роль пользователя не имеет доступа к оплате",
"same_user": "Нельзя оплатить самому себе",
"payment_failed": "Ошибка при обработке платежа"
}
error_message = error_map.get(error_code, "")
payment = None
for p in payments:
if p["id"] == payment_id:
payment = p
break
if not payment:
return '''
<!DOCTYPE html>
<html>
<head>
<title>Платеж не найден</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="alert alert-danger">
<h4>Платеж не найден</h4>
<p>Проверьте правильность ссылки или обратитесь к отправителю.</p>
<a href="/" class="btn btn-primary">На главную</a>
</div>
</div>
</body>
</html>
'''
if payment["status"] != "pending":
status_message = {
"completed": "Оплачен",
"failed": "Не удался",
"cancelled": "Отменен"
}.get(payment["status"], "Неизвестен")
transfer_block = ""
balances = payment.get("balances")
if payment["status"] == "completed" and isinstance(balances, dict):
transfer_block = f"""
<div class="alert alert-success text-start mt-3">
<h6 class="mb-2"><i class="fas fa-check me-2"></i>Перевод выполнен</h6>
<div>Плательщик: <strong>{payment.get('payer', '-')}</strong> | баланс: <strong>{balances.get('payer_before', 0)} ₽</strong> → <strong>{balances.get('payer_after', 0)} ₽</strong></div>
<div>Баланс получателя скрыт</div>
</div>
"""
return f'''
<!DOCTYPE html>
<html>
<head>
<title>Статус платежа</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body text-center">
<h2>Платеж {status_message}</h2>
<div class="my-4">
<i class="fas fa-{ 'check-circle text-success' if payment['status'] == 'completed' else 'times-circle text-danger' } fa-5x"></i>
</div>
<p>ID платежа: <strong>{payment_id}</strong></p>
<p>Сумма: <strong>{payment['amount']} ₽</strong></p>
<p>Получатель: <strong>{payment['receiver']}</strong></p>
<p>Плательщик: <strong>{payment.get('payer', '-')}</strong></p>
<p>Описание: {payment['description']}</p>
{transfer_block}
<a href="/" class="btn btn-primary mt-3">На главную</a>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
'''
allow_any_payer = bool(payment.get("allow_any_payer", False))
payer_label = "Любой пользователь" if allow_any_payer else payment.get("payer", "-")
username_value = "" if allow_any_payer else payment.get("payer", "")
username_readonly = "" if allow_any_payer else "readonly"
error_block = f'<div class="alert alert-danger mb-3">{error_message}</div>' if error_message else ""
return f'''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Оплата #{payment_id}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
:root {{
--primary: #4361ee;
--gradient: linear-gradient(135deg, #4361ee 0%, #3a0ca3 100%);
}}
body {{
background: #f5f7fb;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}}
.payment-container {{
max-width: 500px;
margin: 50px auto;
}}
.payment-card {{
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
overflow: hidden;
}}
.payment-header {{
background: var(--gradient);
color: white;
padding: 25px;
text-align: center;
}}
.payment-body {{
padding: 30px;
}}
.amount-display {{
font-size: 48px;
font-weight: bold;
color: var(--primary);
text-align: center;
margin: 20px 0;
}}
.btn-pay {{
background: var(--gradient);
border: none;
border-radius: 10px;
padding: 15px;
font-size: 18px;
font-weight: 600;
width: 100%;
margin-top: 20px;
}}
</style>
</head>
<body>
<div class="payment-container">
<div class="payment-card">
<div class="payment-header">
<h3><i class="fas fa-credit-card me-2"></i>Оплата</h3>
<p class="mb-0">ID: {payment_id}</p>
</div>
<div class="payment-body">
{error_block}
<div class="text-center mb-4">
<i class="fas fa-shopping-cart fa-4x text-primary"></i>
</div>
<div class="amount-display">
{payment['amount']}
</div>
<div class="payment-details">
<div class="d-flex justify-content-between mb-2">
<span>Получатель:</span>
<strong>{payment['receiver']}</strong>
</div>
<div class="d-flex justify-content-between mb-2">
<span>Плательщик:</span>
<strong>{payer_label}</strong>
</div>
<div class="d-flex justify-content-between mb-2">
<span>Описание:</span>
<strong>{payment['description'] or 'Нет описания'}</strong>
</div>
<div class="d-flex justify-content-between mb-4">
<span>Статус:</span>
<span class="badge bg-warning">Ожидает оплаты</span>
</div>
</div>
<div class="alert alert-secondary small">
Перед списанием система проверит баланс аккаунта плательщика. Если денег недостаточно, платеж не пройдет.
</div>
<form action="/process_payment/{payment_id}" method="post" id="paymentForm">
<div class="mb-3">
<label class="form-label">Ваш логин</label>
<input type="text" name="username" class="form-control" placeholder="Введите ваш логин" required value="{username_value}" {username_readonly}>
</div>
<div class="mb-3">
<label class="form-label">Пароль</label>
<input type="password" name="password" class="form-control" placeholder="Введите пароль" required>
</div>
<button type="submit" class="btn btn-pay">
<i class="fas fa-lock me-2"></i>Оплатить {payment['amount']}
</button>
<div class="text-center mt-3">
<small class="text-muted">После оплаты средства будут переведены получателю</small>
</div>
</form>
<div class="alert alert-info mt-4">
<h6><i class="fas fa-info-circle me-2"></i>Безопасная оплата</h6>
<p class="mb-0 small">Все платежи защищены банковской системой. Для оплаты требуется авторизация.</p>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.getElementById('paymentForm').addEventListener('submit', function(e) {{
const submitBtn = this.querySelector('button[type="submit"]');
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Обработка...';
}});
</script>
</body>
</html>
'''
# ---------- ИСПРАВЛЕННЫЕ API МАРШРУТЫ ----------
@app.route('/api/v1/balance', methods=['GET'])
@api_key_required
@api_roles_required(*FINANCE_ROLES)
def api_balance():
"""API: Получить информацию о балансе - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
try:
user_data = request.user_data
market = load_crypto_market()
market, decayed = apply_serkripto_idle_decay(market)
if decayed:
save_crypto_market(market)
ws_emit_crypto_market_event(market, "idle_decay")
serkripto_qty = get_user_serkripto_amount(user_data)
serkripto_value = round(serkripto_qty * float(market.get("price", 0.0)), 2)
return jsonify({
'success': True,
'username': request.username,
'balance': user_data.get('balance', 0),
'deposit': user_data.get('deposit', 0),
'credit': user_data.get('credit', 0),
'serkripto': {
'coin': market.get('name', SERKRIPTO_NAME),
'symbol': market.get('symbol', SERKRIPTO_SYMBOL),
'quantity': serkripto_qty,
'price_rub': market.get('price', 0),
'value_rub': serkripto_value
},
'total': user_data.get('balance', 0) + user_data.get('deposit', 0) - user_data.get('credit', 0)
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/v1/history', methods=['GET'])
@api_key_required
@api_roles_required(*FINANCE_ROLES)
def api_history():
"""API: Получить историю операций - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
try:
history = load_history()
user_history = [h for h in history if h['user'] == request.username]
limit = request.args.get('limit', type=int, default=50)
if limit > 0 and limit < len(user_history):
user_history = user_history[-limit:]
for h in user_history:
h['time_formatted'] = datetime.fromtimestamp(h['time']).strftime('%Y-%m-%d %H:%M:%S')
return jsonify({
'success': True,
'count': len(user_history),
'history': user_history[::-1]
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/v1/deposit', methods=['POST'])
@api_key_required
@api_roles_required(*FINANCE_ROLES)
def api_deposit():
"""API: Открыть вклад - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
try:
if not request.is_json:
return jsonify({'success': False, 'error': 'JSON data required'}), 400
data = request.get_json()
amount = data.get('amount')
if not amount:
return jsonify({'success': False, 'error': 'Amount required'}), 400
amount = int(amount)
if amount <= 0:
return jsonify({'success': False, 'error': 'Amount must be positive'}), 400
users = load_users()
user = users.get(request.username)
if not user or user['balance'] < amount:
return jsonify({'success': False, 'error': 'Insufficient funds'}), 400
user['balance'] -= amount
user['deposit'] += amount
save_users(users)
add_history(request.username, 'api_deposit', amount)
return jsonify({
'success': True,
'message': f'Deposit of {amount} ₽ created successfully',
'new_balance': user['balance'],
'new_deposit': user['deposit']
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/v1/credit', methods=['POST'])
@api_key_required
@api_roles_required(*CREDIT_ROLES)
def api_credit():
"""API: Взять кредит - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
try:
if not request.is_json:
return jsonify({'success': False, 'error': 'JSON data required'}), 400
data = request.get_json()
amount = data.get('amount')
if not amount:
return jsonify({'success': False, 'error': 'Amount required'}), 400
amount = int(amount)
if amount <= 0:
return jsonify({'success': False, 'error': 'Amount must be positive'}), 400
users = load_users()
user = users.get(request.username)
if not user:
return jsonify({'success': False, 'error': 'User not found'}), 404
user['balance'] += amount
user['credit'] += amount
save_users(users)
add_history(request.username, 'api_credit', amount)
return jsonify({
'success': True,
'message': f'Credit of {amount} ₽ taken successfully',
'new_balance': user['balance'],
'new_credit': user['credit']
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/v1/transfer', methods=['POST'])
@api_key_required
@api_roles_required(*FINANCE_ROLES)
def api_transfer():
"""API: Сделать перевод - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
try:
if not request.is_json:
return jsonify({'success': False, 'error': 'JSON data required'}), 400
data = request.get_json()
target = data.get('target', '').strip()
amount = data.get('amount')
if not target:
return jsonify({'success': False, 'error': 'Target username required'}), 400
if not amount:
return jsonify({'success': False, 'error': 'Amount required'}), 400
amount = int(amount)
if amount <= 0:
return jsonify({'success': False, 'error': 'Amount must be positive'}), 400
users = load_users()
if target not in users:
return jsonify({'success': False, 'error': 'Target user not found'}), 404
if target == request.username:
return jsonify({'success': False, 'error': 'Cannot transfer to yourself'}), 400
sender = users.get(request.username)
if not sender:
return jsonify({'success': False, 'error': 'Sender not found'}), 404
if sender['balance'] < amount:
return jsonify({'success': False, 'error': 'Insufficient funds'}), 400
sender['balance'] -= amount
users[target]['balance'] += amount
save_users(users)
add_history(request.username, 'api_transfer', amount, target)
add_history(target, 'transfer_received', amount, request.username)
ws_emit_balance_update(target)
ws_emit_user(target, "transfer.received", {
"from": request.username,
"amount": amount
})
ws_emit_system("transfer.completed", {
"from": request.username,
"to": target,
"amount": amount,
"source": "api"
})
return jsonify({
'success': True,
'message': f'Transfer of {amount} ₽ to {target} successful',
'new_balance': sender['balance']
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
# ---------- API ДЛЯ ПЛАТЕЖЕЙ ----------
@app.route('/api/v1/payment/create', methods=['POST'])
@api_key_required
@api_roles_required(*FINANCE_ROLES)
def api_payment_create():
"""API: Создать платеж"""
try:
if not request.is_json:
return jsonify({'success': False, 'error': 'JSON data required'}), 400
data = request.get_json()
receiver = str(data.get('receiver', '')).strip()
payer_from_request = str(data.get('payer', '')).strip()
allow_any_payer = bool(data.get('allow_any_payer', False))
amount = data.get('amount')
description = data.get('description', '')
external_id = data.get('external_id', '')
if not receiver:
return jsonify({'success': False, 'error': 'Receiver required'}), 400
if not amount:
return jsonify({'success': False, 'error': 'Amount required'}), 400
try:
amount_int = int(amount)
if amount_int <= 0:
return jsonify({'success': False, 'error': 'Amount must be positive'}), 400
except:
return jsonify({'success': False, 'error': 'Invalid amount'}), 400
# Проверяем существование получателя
users = load_users()
if receiver not in users:
return jsonify({'success': False, 'error': 'Receiver not found'}), 404
payer = request.username
if payer_from_request:
if payer_from_request in ["*", "any", "ANY"]:
allow_any_payer = True
payer = "*"
else:
payer = payer_from_request
allow_any_payer = False
elif allow_any_payer:
payer = "*"
if not allow_any_payer:
if payer not in users:
return jsonify({'success': False, 'error': 'Payer not found'}), 404
if payer == receiver:
return jsonify({'success': False, 'error': 'Payer and receiver must be different'}), 400
# Создаем платеж
payment_id, payment_data = create_payment(
payer=payer,
receiver=receiver,
amount=amount_int,
description=description,
external_id=external_id,
allow_any_payer=allow_any_payer,
created_by=request.username
)
return jsonify({
'success': True,
'message': 'Payment created successfully',
'payment_id': payment_id,
'payment': payment_data,
'payment_url': f'{request.host_url.rstrip("/")}/pay/{payment_id}'
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/v1/payment/status/<payment_id>', methods=['GET'])
@api_key_required
@api_roles_required(*FINANCE_ROLES)
def api_payment_status(payment_id):
"""API: Получить статус платежа"""
try:
payments = load_payments()
payment = None
for p in payments:
if p["id"] == payment_id:
payment = p
break
if not payment:
return jsonify({'success': False, 'error': 'Payment not found'}), 404
# Проверяем, имеет ли пользователь доступ к этому платежу
if (payment["payer"] != request.username and
payment["receiver"] != request.username and
payment.get("created_by") != request.username):
if getattr(request, "user_role", ROLE_CLIENT) != ROLE_ADMIN:
return jsonify({'success': False, 'error': 'Access denied'}), 403
return jsonify({
'success': True,
'payment': sanitize_payment_output(payment)
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/v1/payment/list', methods=['GET'])
@api_key_required
@api_roles_required(*FINANCE_ROLES)
def api_payment_list():
"""API: Получить список платежей пользователя"""
try:
payments = load_payments()
# Фильтруем платежи по пользователю
user_payments = []
for payment in payments:
if (payment["payer"] == request.username or
payment["receiver"] == request.username or
payment.get("created_by") == request.username):
user_payments.append(payment)
# Сортируем по дате (новые сверху)
user_payments.sort(key=lambda x: x['created'], reverse=True)
# Ограничиваем количество
limit = min(20, len(user_payments))
user_payments = user_payments[:limit]
safe_payments = [sanitize_payment_output(payment) for payment in user_payments]
return jsonify({
'success': True,
'count': len(safe_payments),
'payments': safe_payments
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/v1/shop/items', methods=['GET'])
@api_key_required
@api_roles_required(*FINANCE_ROLES)
def api_shop_items():
"""API: Получить список товаров - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
try:
shop_data = load_shop()
available_items = [item for item in shop_data["items"] if item["status"] == "available"]
for item in available_items:
item['created_formatted'] = datetime.fromtimestamp(item['created']).strftime('%Y-%m-%d %H:%M:%S')
return jsonify({
'success': True,
'count': len(available_items),
'items': available_items[::-1]
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/v1/shop/ads', methods=['GET'])
@api_key_required
@api_roles_required(*FINANCE_ROLES)
def api_shop_ads():
"""API: Получить список объявлений - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
try:
shop_data = load_shop()
active_ads = [ad for ad in shop_data["ads"] if ad["status"] == "active"]
for ad in active_ads:
ad['created_formatted'] = datetime.fromtimestamp(ad['created']).strftime('%Y-%m-%d %H:%M:%S')
return jsonify({
'success': True,
'count': len(active_ads),
'ads': active_ads[::-1]
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/v1/crypto', methods=['GET'])
@api_key_required
@api_roles_required(*CRYPTO_ROLES)
def api_crypto_market():
"""API: Получить состояние рынка SerKripto"""
try:
users = load_users()
user_data = users.get(request.username, {})
market = load_crypto_market()
market, decayed = apply_serkripto_idle_decay(market)
if decayed:
save_crypto_market(market)
ws_emit_crypto_market_event(market, "idle_decay")
qty = get_user_serkripto_amount(user_data)
price = float(market.get("price", 0.0))
value = round(qty * price, 2)
return jsonify({
"success": True,
"coin": {
"name": market.get("name", SERKRIPTO_NAME),
"symbol": market.get("symbol", SERKRIPTO_SYMBOL),
"price_rub": round(price, 4),
"min_price_rub": round(float(market.get("min_price", 1.0)), 4),
"max_price_rub": round(float(market.get("max_price", 1000000.0)), 4),
"decay_rate_pct": round(float(market.get("idle_decay_rate", 0.0)) * 100, 3),
"decay_interval_sec": int(market.get("idle_decay_interval_sec", 30)),
"stats": market.get("stats", {})
},
"wallet": {
"quantity": qty,
"value_rub": value,
"balance_rub": int(user_data.get("balance", 0)) if isinstance(user_data, dict) else 0
},
"updated_at": int(time.time())
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/v1/crypto/buy', methods=['POST'])
@api_key_required
@api_roles_required(*CRYPTO_ROLES)
def api_crypto_buy():
"""API: Купить SerKripto за рубли"""
try:
if not request.is_json:
return jsonify({'success': False, 'error': 'JSON data required'}), 400
data = request.get_json()
amount = data.get("amount")
success, result = buy_serkripto(request.username, amount)
if not success:
error_map = {
"invalid_amount": "Amount must be a positive integer",
"user_not_found": "User not found",
"market_unavailable": "Crypto market unavailable",
"insufficient_funds": "Insufficient funds",
"amount_too_small": "Amount is too small"
}
status_map = {
"user_not_found": 404,
"market_unavailable": 503
}
return jsonify({
"success": False,
"error": error_map.get(result, "Buy operation failed"),
"code": result
}), status_map.get(result, 400)
return jsonify({
"success": True,
"message": f"{SERKRIPTO_NAME} purchased successfully",
"trade": result
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/v1/crypto/sell', methods=['POST'])
@api_key_required
@api_roles_required(*CRYPTO_ROLES)
def api_crypto_sell():
"""API: Продать SerKripto"""
try:
if not request.is_json:
return jsonify({'success': False, 'error': 'JSON data required'}), 400
data = request.get_json()
quantity = data.get("quantity")
success, result = sell_serkripto(request.username, quantity)
if not success:
error_map = {
"invalid_quantity": "Quantity must be a positive number",
"user_not_found": "User not found",
"market_unavailable": "Crypto market unavailable",
"insufficient_crypto": "Insufficient crypto balance",
"amount_too_small": "Quantity is too small"
}
status_map = {
"user_not_found": 404,
"market_unavailable": 503
}
return jsonify({
"success": False,
"error": error_map.get(result, "Sell operation failed"),
"code": result
}), status_map.get(result, 400)
return jsonify({
"success": True,
"message": f"{SERKRIPTO_NAME} sold successfully",
"trade": result
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/v1/system/status', methods=['GET'])
@api_key_required
@api_roles_required(ROLE_ADMIN, ROLE_MANAGER)
def api_system_status():
"""API: Получить статус системы - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
try:
users = load_users()
valid_users = [u for u in users.values() if isinstance(u, dict)]
total_users = len(valid_users)
total_balance = sum(u.get('balance', 0) for u in valid_users)
total_deposit = sum(u.get('deposit', 0) for u in valid_users)
total_credit = sum(u.get('credit', 0) for u in valid_users)
total_serkripto = round(sum(get_user_serkripto_amount(u) for u in valid_users), 6)
crypto_market = load_crypto_market()
crypto_market, decayed = apply_serkripto_idle_decay(crypto_market)
if decayed:
save_crypto_market(crypto_market)
ws_emit_crypto_market_event(crypto_market, "idle_decay")
return jsonify({
'success': True,
'system_status': 'operational',
'statistics': {
'total_users': total_users,
'total_balance': total_balance,
'total_deposit': total_deposit,
'total_credit': total_credit,
'net_worth': total_balance + total_deposit - total_credit,
'serkripto_user_holdings': total_serkripto
},
'crypto': {
'name': crypto_market.get('name', SERKRIPTO_NAME),
'symbol': crypto_market.get('symbol', SERKRIPTO_SYMBOL),
'price_rub': crypto_market.get('price', 0)
},
'timestamp': int(time.time())
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
# ДОБАВЛЕННЫЕ API ЭНДПОИНТЫ
@app.route('/api/v1/users', methods=['GET'])
@api_key_required
@api_roles_required(ROLE_ADMIN)
def api_users():
"""API: Получить список пользователей (только для админов)"""
try:
users = load_users()
safe_users = {}
for username, user in users.items():
if isinstance(user, dict):
safe_user = user.copy()
safe_user.pop('password', None)
safe_users[username] = safe_user
return jsonify({
'success': True,
'count': len(safe_users),
'users': safe_users
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/v1/health', methods=['GET'])
def api_health():
"""API: Проверка здоровья системы (не требует API ключа)"""
try:
files = [USERS_FILE, HISTORY_FILE, SHOP_FILE, PAYMENTS_FILE, JOURNAL_FILE, CLASSES_FILE, SUBJECTS_FILE, PARENT_LINKS_FILE, CRYPTO_FILE]
file_status = {}
for file in files:
file_status[file] = os.path.exists(file)
return jsonify({
'success': True,
'status': 'healthy',
'timestamp': int(time.time()),
'files': file_status,
'message': 'System is operational'
})
except Exception as e:
return jsonify({'success': False, 'status': 'unhealthy', 'error': str(e)}), 500
@app.route('/api/v1/shop/create_item', methods=['POST'])
@api_key_required
@api_roles_required(*SHOP_SELL_ROLES)
def api_create_shop_item():
"""API: Создать товар в магазине"""
try:
if not request.is_json:
return jsonify({'success': False, 'error': 'JSON data required'}), 400
data = request.get_json()
title = data.get('title')
description = data.get('description')
price = data.get('price')
category = data.get('category', 'other')
if not title or not description or not price:
return jsonify({'success': False, 'error': 'Title, description and price required'}), 400
try:
price_int = int(price)
if price_int <= 0:
return jsonify({'success': False, 'error': 'Price must be positive'}), 400
except:
return jsonify({'success': False, 'error': 'Invalid price'}), 400
item_id = create_shop_item(request.username, title, description, price_int, category)
return jsonify({
'success': True,
'message': f'Item "{title}" created successfully',
'item_id': item_id
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/v1/shop/buy_item', methods=['POST'])
@api_key_required
@api_roles_required(*FINANCE_ROLES)
def api_buy_item():
"""API: Купить товар"""
try:
if not request.is_json:
return jsonify({'success': False, 'error': 'JSON data required'}), 400
data = request.get_json()
item_id = data.get('item_id')
if not item_id:
return jsonify({'success': False, 'error': 'Item ID required'}), 400
success, message = buy_item(item_id, request.username)
return jsonify({
'success': success,
'message': message
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
# ---------- МАГАЗИН МАРШРУТЫ ----------
@app.route('/create_shop_item', methods=['POST'])
@login_required
@web_roles_required(*SHOP_SELL_ROLES)
def create_shop_item_route():
"""Создание товара в магазине"""
title = request.form.get('title')
description = request.form.get('description')
price = request.form.get('price')
category = request.form.get('category', 'other')
if not title or not description or not price:
return redirect('/dashboard')
try:
price_int = int(price)
if price_int <= 0:
return redirect('/dashboard')
except:
return redirect('/dashboard')
item_id = create_shop_item(session['user'], title, description, price_int, category)
return redirect('/dashboard')
@app.route('/create_advertisement', methods=['POST'])
@login_required
@web_roles_required(*SHOP_SELL_ROLES)
def create_advertisement_route():
"""Создание объявления"""
title = request.form.get('title')
content = request.form.get('content')
contact_info = request.form.get('contact_info')
if not title or not content or not contact_info:
return redirect('/dashboard')
ad_id = create_advertisement(session['user'], title, content, contact_info)
return redirect('/dashboard')
@app.route('/buy_item/<item_id>', methods=['POST'])
@login_required
@web_roles_required(*FINANCE_ROLES)
def buy_item_route(item_id):
"""Покупка товара"""
success, message = buy_item(item_id, session['user'])
return redirect('/dashboard')
@app.route('/crypto/buy', methods=['POST'])
@login_required
@web_roles_required(*CRYPTO_ROLES)
def crypto_buy_route():
amount = request.form.get('amount', '').strip()
success, result = buy_serkripto(session['user'], amount)
if success:
return redirect('/dashboard?tab=crypto&crypto_status=buy_success')
return redirect(f'/dashboard?tab=crypto&crypto_error={result}')
@app.route('/crypto/sell', methods=['POST'])
@login_required
@web_roles_required(*CRYPTO_ROLES)
def crypto_sell_route():
quantity = request.form.get('quantity', '').strip()
success, result = sell_serkripto(session['user'], quantity)
if success:
return redirect('/dashboard?tab=crypto&crypto_status=sell_success')
return redirect(f'/dashboard?tab=crypto&crypto_error={result}')
# ---------- ПЛАТЕЖНЫЕ МАРШРУТЫ ----------
@app.route('/payment_page')
@login_required
@web_roles_required(*FINANCE_ROLES)
def payment_page():
"""Страница управления платежами"""
created_payment = None
created_payment_id = request.args.get('created', '').strip()
if created_payment_id:
created_payment = get_payment_by_id(created_payment_id)
error_map = {
"missing_fields": "Заполните получателя и сумму",
"invalid_amount": "Сумма должна быть положительным числом",
"receiver_not_found": "Получатель не найден",
"payer_not_found": "Плательщик не найден",
"payer_role_denied": "У выбранного плательщика нет доступа к оплате",
"same_users": "Плательщик и получатель не должны совпадать"
}
error_message = error_map.get(request.args.get('error', '').strip(), "")
return render_payment_page(created_payment=created_payment, error_message=error_message)
@app.route('/create_payment_link', methods=['POST'])
@login_required
@web_roles_required(*FINANCE_ROLES)
def create_payment_link():
"""Создание ссылки для оплаты"""
receiver = request.form.get('receiver', '').strip()
payer = request.form.get('payer', '').strip()
amount = request.form.get('amount', '').strip()
description = request.form.get('description', '').strip()
if not receiver or not amount:
return redirect('/payment_page?error=missing_fields')
try:
amount_int = int(amount)
if amount_int <= 0:
return redirect('/payment_page?error=invalid_amount')
except:
return redirect('/payment_page?error=invalid_amount')
users = load_users()
if receiver not in users:
return redirect('/payment_page?error=receiver_not_found')
allow_any_payer = False
payer_for_payment = payer
if payer:
if payer not in users:
return redirect('/payment_page?error=payer_not_found')
if not has_role(users.get(payer, {}), FINANCE_ROLES):
return redirect('/payment_page?error=payer_role_denied')
if payer == receiver:
return redirect('/payment_page?error=same_users')
else:
allow_any_payer = True
payer_for_payment = "*"
payment_id, payment_data = create_payment(
payer=payer_for_payment,
receiver=receiver,
amount=amount_int,
description=description,
allow_any_payer=allow_any_payer,
created_by=session['user']
)
return redirect(f'/payment_page?created={payment_id}')
@app.route('/pay/<payment_id>', methods=['GET'])
def pay_page(payment_id):
"""Страница оплаты для клиента"""
return render_pay_page(payment_id)
@app.route('/process_payment/<payment_id>', methods=['POST'])
def process_payment_route(payment_id):
"""Обработка платежа (для неавторизованных пользователей)"""
username = request.form.get('username', '').strip()
password = request.form.get('password', '').strip()
if not username or not password:
return redirect(f'/pay/{payment_id}?error=invalid_credentials')
# Проверяем логин и пароль
users = load_users()
user_data = users.get(username)
if not user_data or user_data['password'] != password:
return redirect(f'/pay/{payment_id}?error=invalid_credentials')
if not has_role(user_data, FINANCE_ROLES):
return redirect(f'/pay/{payment_id}?error=role_denied')
# Проверяем, что пользователь является плательщиком
payments = load_payments()
payment = None
for p in payments:
if p["id"] == payment_id:
payment = p
break
if not payment:
return redirect(f'/pay/{payment_id}?error=payment_not_found')
if not payment.get("allow_any_payer", False) and payment["payer"] != username:
return redirect(f'/pay/{payment_id}?error=wrong_payer')
# Выполняем платеж
success, message = process_payment(payment_id, username)
if success:
return redirect(f'/pay/{payment_id}?paid=1')
else:
ws_emit_user(username, "payment.failed", {
"payment_id": payment_id,
"reason": message
})
ws_emit_system("payment.failed", {
"payment_id": payment_id,
"user": username,
"reason": message,
"source": "web"
})
error_map = {
"Недостаточно средств": "insufficient_funds",
"Платеж не найден": "payment_not_found",
"Неверный плательщик": "wrong_payer",
"Плательщик не найден": "payer_not_found",
"Получатель не найден": "receiver_not_found",
"Нельзя оплатить самому себе": "same_user"
}
error_code = error_map.get(message, "payment_failed")
return redirect(f'/pay/{payment_id}?error={error_code}')
# ---------- ОСНОВНЫЕ МАРШРУТЫ ----------
@app.route('/', methods=['GET', 'POST'])
def login():
if request.args.get("reset_2fa", "").strip():
clear_pending_2fa_session()
return render_login()
if request.method == 'POST':
users = load_users()
login_step = request.form.get("login_step", "").strip().lower()
if login_step == "2fa":
pending_username = str(session.get("pending_2fa_user", "")).strip()
if not pending_username or not is_pending_2fa_alive():
clear_pending_2fa_session()
return render_login("Срок подтверждения 2FA истек. Выполните вход снова.")
user_data = users.get(pending_username)
if not isinstance(user_data, dict):
clear_pending_2fa_session()
return render_login("Пользователь не найден. Выполните вход снова.")
if not parse_bool(user_data.get("twofa_enabled", False)):
clear_pending_2fa_session()
return render_login("2FA уже отключена. Выполните вход по паролю.")
twofa_secret = user_data.get("twofa_secret", "")
if not twofa_secret:
clear_pending_2fa_session()
return render_login("Секрет 2FA отсутствует. Выполните вход снова.")
twofa_code = request.form.get("twofa_code", "").strip()
if not verify_totp_code(twofa_secret, twofa_code):
return render_login("Неверный код 2FA", twofa_mode=True, username_hint=pending_username)
session['user'] = pending_username
clear_pending_2fa_session()
ws_emit_user(pending_username, "auth.login", {"username": pending_username, "twofa": True})
ws_emit_system("auth.login", {"username": pending_username, "twofa": True})
ws_emit_balance_update(pending_username)
return redirect('/dashboard')
username = request.form.get('username', '').strip()
password = request.form.get('password', '').strip()
if not username or not password:
clear_pending_2fa_session()
return render_login("Введите логин и пароль")
user_data = users.get(username)
if user_data and isinstance(user_data, dict) and user_data['password'] == password:
if parse_bool(user_data.get("twofa_enabled", False)) and user_data.get("twofa_secret"):
session["pending_2fa_user"] = username
session["pending_2fa_started"] = int(time.time())
return render_login(twofa_mode=True, username_hint=username)
session['user'] = username
clear_pending_2fa_session()
ws_emit_user(username, "auth.login", {"username": username})
ws_emit_system("auth.login", {"username": username})
ws_emit_balance_update(username)
return redirect('/dashboard')
clear_pending_2fa_session()
return render_login("Неверный логин или пароль")
if session.get("pending_2fa_user"):
if is_pending_2fa_alive():
return render_login(twofa_mode=True, username_hint=session.get("pending_2fa_user"))
clear_pending_2fa_session()
return render_login()
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
users = load_users()
username = request.form.get('username', '').strip()
if username in users:
return render_register().replace('</form>',
'<div class="alert alert-danger mt-3">Пользователь уже существует</div></form>')
user_data = {
'password': request.form.get('password'),
'balance': 1000,
'deposit': 0,
'credit': 0,
'serkripto': 0.0,
'role': ROLE_CLIENT,
'full_name': f"{request.form.get('first_name')} {request.form.get('last_name')}",
'email': request.form.get('email'),
'api_key': None,
'twofa_enabled': False,
'twofa_secret': ""
}
users[username] = user_data
save_users(users)
session['user'] = username
ws_emit_user(username, "auth.register", {"username": username})
ws_emit_system("auth.register", {"username": username})
ws_emit_balance_update(username)
return redirect('/dashboard')
return render_register()
@app.route('/dashboard')
@login_required
def dashboard():
users = load_users()
history = load_history()
if session['user'] not in users:
session.clear()
return redirect('/')
user_data = users[session['user']]
user_history = [h for h in history if h['user'] == session['user']]
shop_data = load_shop()
crypto_market = load_crypto_market()
crypto_flash = None
crypto_status = request.args.get('crypto_status', '').strip()
crypto_error = request.args.get('crypto_error', '').strip()
crypto_status_map = {
"buy_success": "Покупка SerKripto выполнена",
"sell_success": "Продажа SerKripto выполнена"
}
crypto_error_map = {
"invalid_amount": "Введите корректную сумму покупки",
"invalid_quantity": "Введите корректное количество монет",
"insufficient_funds": "Недостаточно рублей для покупки",
"insufficient_crypto": "Недостаточно SerKripto на кошельке",
"market_unavailable": "Рынок SerKripto временно недоступен",
"user_not_found": "Пользователь не найден",
"amount_too_small": "Слишком маленький объем операции"
}
if crypto_status in crypto_status_map:
crypto_flash = {"class": "success", "text": crypto_status_map[crypto_status]}
elif crypto_error in crypto_error_map:
crypto_flash = {"class": "danger", "text": crypto_error_map[crypto_error]}
requested_tab = request.args.get('tab', '').strip().lower()
return render_dashboard(
user_data,
user_history[-20:],
shop_data,
crypto_market=crypto_market,
crypto_flash=crypto_flash,
active_tab=requested_tab or "services"
)
@app.route('/generate_api_key', methods=['POST'])
@login_required
@web_roles_required(*FINANCE_ROLES)
def generate_api_key_route():
users = load_users()
user = users.get(session['user'])
if user and isinstance(user, dict):
user['api_key'] = generate_api_key()
save_users(users)
add_history(session['user'], 'generate_api_key', 0)
return redirect('/dashboard')
@app.route('/deposit', methods=['POST'])
@login_required
@web_roles_required(*FINANCE_ROLES)
def deposit():
users = load_users()
user = users.get(session['user'])
try:
amount = int(request.form.get('amount', 0))
if amount > 0 and user and user['balance'] >= amount:
user['balance'] -= amount
user['deposit'] += amount
save_users(users)
add_history(session['user'], 'deposit', amount)
except:
pass
return redirect('/dashboard')
@app.route('/credit', methods=['POST'])
@login_required
@web_roles_required(*CREDIT_ROLES)
def credit():
users = load_users()
user = users.get(session['user'])
try:
amount = int(request.form.get('amount', 0))
if amount > 0 and user:
user['balance'] += amount
user['credit'] += amount
save_users(users)
add_history(session['user'], 'credit', amount)
except:
pass
return redirect('/dashboard')
@app.route('/transfer', methods=['POST'])
@login_required
@web_roles_required(*FINANCE_ROLES)
def transfer():
users = load_users()
sender = session['user']
try:
target = request.form.get('target')
amount = int(request.form.get('amount', 0))
sender_data = users.get(sender)
target_data = users.get(target)
if (amount > 0 and target_data and target != sender and
sender_data and sender_data['balance'] >= amount):
sender_data['balance'] -= amount
target_data['balance'] += amount
save_users(users)
add_history(sender, 'transfer', amount, target)
add_history(target, 'transfer_received', amount, sender)
ws_emit_balance_update(target)
ws_emit_user(target, "transfer.received", {
"from": sender,
"amount": amount
})
ws_emit_system("transfer.completed", {
"from": sender,
"to": target,
"amount": amount,
"source": "web"
})
except Exception as e:
print(f"Transfer error: {e}")
pass
return redirect('/dashboard')
# ---------- АДМИН МАРШРУТЫ (ИСПРАВЛЕННЫЕ) ----------
@app.route('/admin')
@admin_required
def admin():
users = load_users()
return render_admin_panel(users)
@app.route('/admin/change_role', methods=['POST'])
@admin_required
def admin_change_role():
users = load_users()
username = request.form.get('username')
new_role = normalize_role(request.form.get('role'))
if username in users and username != session['user'] and isinstance(users[username], dict):
users[username]['role'] = new_role
save_users(users)
add_history(session['user'], 'change_role', 0, f"{username}->{new_role}")
ws_emit_user(username, "role.changed", {"role": new_role})
ws_emit_system("role.changed", {
"username": username,
"role": new_role,
"by": session.get("user")
})
return redirect('/admin')
@app.route('/admin/add_money', methods=['POST'])
@admin_required
def admin_add_money():
users = load_users()
username = request.form.get('username')
amount = int(request.form.get('amount', 0))
if username in users and amount > 0 and isinstance(users[username], dict):
users[username]['balance'] += amount
save_users(users)
add_history(session['user'], 'add_money', amount, username)
ws_emit_balance_update(username)
ws_emit_user(username, "admin.add_money", {
"amount": amount,
"by": session.get("user")
})
ws_emit_system("admin.add_money", {
"username": username,
"amount": amount,
"by": session.get("user")
})
return redirect('/admin')
@app.route('/logout')
def logout():
username = session.get('user')
if username:
ws_emit_user(username, "auth.logout", {"username": username})
ws_emit_system("auth.logout", {"username": username})
session.clear()
return redirect('/')
# ---------- API ДОКУМЕНТАЦИЯ ----------
@app.route('/api/docs')
def api_docs():
"""Страница с документацией API"""
return '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Документация</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
background: #f5f7fb;
padding: 20px;
}
.code-block {
background: #1e1e1e;
color: #d4d4d4;
padding: 15px;
border-radius: 5px;
font-family: "Courier New", monospace;
overflow-x: auto;
margin: 10px 0;
}
.endpoint {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 3px 10px rgba(0,0,0,0.05);
border-left: 4px solid #4361ee;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-12">
<div class="card mb-4">
<div class="card-body">
<h1 class="card-title"><i class="fas fa-code me-2"></i>Документация API</h1>
<p class="card-text">Используйте API для интеграции банковских услуг в ваши приложения.</p>
<h3 class="mt-4">Базовый URL</h3>
<div class="code-block">
http://localhost:5000/api/v1/
</div>
<h3 class="mt-4">Аутентификация</h3>
<p>Для использования API необходим API ключ. Получите его в личном кабинете.</p>
<p>Передавайте ключ одним из способов:</p>
<h5>Заголовок HTTP:</h5>
<div class="code-block">
X-API-Key: ваш_api_ключ
</div>
<h5>Параметр запроса:</h5>
<div class="code-block">
?api_key=ваш_api_ключ
</div>
<h5>JSON тело запроса:</h5>
<div class="code-block">
{
"api_key": "ваш_api_ключ",
...
}
</div>
<h3 class="mt-4">Доступные эндпоинты</h3>
<div class="endpoint">
<h5><span class="badge bg-success">GET</span> /api/v1/health</h5>
<p>Проверка здоровья системы (не требует API ключа)</p>
<div class="code-block">
curl http://localhost:5000/api/v1/health
</div>
</div>
<div class="endpoint">
<h5><span class="badge bg-info">GET</span> /api/v1/balance</h5>
<p>Получить информацию о балансе пользователя</p>
<div class="code-block">
curl -H "X-API-Key: ваш_ключ" http://localhost:5000/api/v1/balance
</div>
</div>
<div class="endpoint">
<h5><span class="badge bg-info">GET</span> /api/v1/history?limit=50</h5>
<p>Получить историю операций (опциональный параметр limit)</p>
</div>
<div class="endpoint">
<h5><span class="badge bg-success">POST</span> /api/v1/deposit</h5>
<p>Открыть вклад. Требуется JSON: {"amount": 100}</p>
<div class="code-block">
curl -X POST -H "X-API-Key: ваш_ключ" -H "Content-Type: application/json"<br>
-d "{\\"amount\\": 100}" http://localhost:5000/api/v1/deposit
</div>
</div>
<div class="endpoint">
<h5><span class="badge bg-success">POST</span> /api/v1/credit</h5>
<p>Взять кредит. Требуется JSON: {"amount": 100}</p>
</div>
<div class="endpoint">
<h5><span class="badge bg-success">POST</span> /api/v1/transfer</h5>
<p>Сделать перевод. Требуется JSON: {"target": "username", "amount": 100}</p>
</div>
<div class="endpoint">
<h5><span class="badge bg-info">GET</span> /api/v1/shop/items</h5>
<p>Получить список товаров в магазине</p>
</div>
<div class="endpoint">
<h5><span class="badge bg-info">GET</span> /api/v1/crypto</h5>
<p>Получить текущий курс SerKripto и баланс монет пользователя</p>
</div>
<div class="endpoint">
<h5><span class="badge bg-success">POST</span> /api/v1/crypto/buy</h5>
<p>Купить SerKripto. Требуется JSON: {"amount": 1000}</p>
</div>
<div class="endpoint">
<h5><span class="badge bg-success">POST</span> /api/v1/crypto/sell</h5>
<p>Продать SerKripto. Требуется JSON: {"quantity": 0.25}</p>
</div>
<div class="endpoint">
<h5><span class="badge bg-success">POST</span> /api/v1/payment/create</h5>
<p>Создать платеж. Требуется JSON: {"receiver": "...", "amount": 100, "description": "..."}</p>
</div>
<div class="endpoint">
<h5><span class="badge bg-info">GET</span> /api/v1/payment/status/{payment_id}</h5>
<p>Получить статус платежа</p>
</div>
<div class="endpoint">
<h5><span class="badge bg-info">GET</span> /api/v1/system/status</h5>
<p>Получить статус системы</p>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
'''
# ================= ДЕКОРАТОР УЧИТЕЛЯ =================
def teacher_required(f):
return web_roles_required(*JOURNAL_ROLES)(f)
def student_required(f):
return web_roles_required(ROLE_STUDENT, ROLE_ADMIN)(f)
def parent_required(f):
return web_roles_required(ROLE_PARENT, ROLE_ADMIN)(f)
def manager_required(f):
return web_roles_required(ROLE_MANAGER, ROLE_ADMIN)(f)
def get_student_class(student):
classes = load_classes()
for class_name, students in classes.items():
if student in students:
return class_name
return "-"
def get_student_stats(student):
journal = load_journal()
users = load_users()
subjects = journal.get(student, {})
all_grades = []
for grades in subjects.values():
all_grades.extend(grades)
avg = round(sum(all_grades) / len(all_grades), 2) if all_grades else 0
grade_count = len(all_grades)
finance_delta = sum(calculate_money_for_grade(g) for g in all_grades)
current_balance = users.get(student, {}).get("balance", 0)
return {
"avg": avg,
"grade_count": grade_count,
"finance_delta": finance_delta,
"current_balance": current_balance,
"subjects": subjects
}
def get_grade_events_from_history(student):
history = load_history()
events = []
pattern = re.compile(r"^(.*?):\s*оценка\s*([2-5])\s*\(([+-]?\d+)\)")
for item in history:
if item.get("user") != student or item.get("action") != "grade_update":
continue
target = str(item.get("target", ""))
match = pattern.match(target)
if match:
subject = match.group(1).strip() or "Предмет"
grade = int(match.group(2))
delta = int(match.group(3))
else:
subject = "Предмет"
grade = 0
delta = 0
events.append({
"time": int(item.get("time", 0)),
"subject": subject,
"grade": grade,
"delta": delta
})
events.sort(key=lambda x: x["time"])
return events
def render_student_panel(student_username):
users = load_users()
if student_username not in users:
return "Ученик не найден", 404
stats = get_student_stats(student_username)
class_name = get_student_class(student_username)
grade_events = get_grade_events_from_history(student_username)
subject_rows = ""
for subject, grades in stats["subjects"].items():
subject_avg = round(sum(grades) / len(grades), 2) if grades else 0
subject_rows += f"""
<tr>
<td>{subject}</td>
<td>{', '.join(map(str, grades)) if grades else '-'}</td>
<td>{subject_avg}</td>
</tr>
"""
if not subject_rows:
subject_rows = "<tr><td colspan='3'>Нет оценок</td></tr>"
timeline_rows = ""
labels = []
grades_series = []
avg_series = []
money_series = []
positive_total = 0
negative_total = 0
cumulative_money = 0
running_grades = []
for index, event in enumerate(grade_events, start=1):
event_time = datetime.fromtimestamp(event["time"]).strftime("%d.%m.%Y %H:%M") if event["time"] else "-"
delta = int(event["delta"])
cumulative_money += delta
if delta >= 0:
positive_total += delta
delta_color = "text-success"
delta_text = f"+{delta}"
else:
negative_total += abs(delta)
delta_color = "text-danger"
delta_text = str(delta)
timeline_rows += f"""
<tr>
<td>{index}</td>
<td>{event_time}</td>
<td>{event['subject']}</td>
<td>{event['grade'] if event['grade'] else '-'}</td>
<td class="{delta_color}"><b>{delta_text}</b></td>
<td><b>{cumulative_money}</b></td>
</tr>
"""
labels.append(index)
if event["grade"]:
running_grades.append(event["grade"])
grades_series.append(event["grade"])
avg_series.append(round(sum(running_grades) / len(running_grades), 2))
else:
grades_series.append(None)
avg_series.append(avg_series[-1] if avg_series else 0)
money_series.append(cumulative_money)
if not timeline_rows:
timeline_rows = "<tr><td colspan='6'>Пока нет оценок и начислений</td></tr>"
if not labels:
labels = [1]
grades_series = [None]
avg_series = [0]
money_series = [0]
net_money = positive_total - negative_total
stats_net = stats["finance_delta"]
if not grade_events and stats_net != 0:
net_money = stats_net
if stats_net > 0:
positive_total = stats_net
else:
negative_total = abs(stats_net)
net_color = "text-success" if net_money >= 0 else "text-danger"
current_role = get_user_role(users.get(session.get("user"), {}))
role_view = "Панель ученика"
back_link = "/dashboard"
if current_role == ROLE_ADMIN:
role_view = "Панель ученика (режим администратора)"
elif current_role == ROLE_PARENT:
role_view = "Панель ученика (просмотр родителя)"
back_link = "/parent_panel"
elif current_role == ROLE_MANAGER:
role_view = "Панель ученика (просмотр менеджера)"
back_link = "/manager_panel"
return f"""
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Панель ученика</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
body {{
background: linear-gradient(180deg, #f6f9ff 0%, #eef3ff 100%);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}}
.top-card {{
background: linear-gradient(135deg, #1d4ed8, #2563eb);
color: white;
border-radius: 18px;
padding: 24px;
box-shadow: 0 12px 26px rgba(37, 99, 235, 0.28);
}}
.metric {{
background: white;
border-radius: 14px;
padding: 16px;
border: 1px solid #e6ecff;
box-shadow: 0 5px 14px rgba(60, 90, 170, 0.1);
height: 100%;
}}
.table-card {{
background: white;
border-radius: 14px;
padding: 16px;
border: 1px solid #e6ecff;
box-shadow: 0 5px 14px rgba(60, 90, 170, 0.1);
margin-top: 16px;
}}
.badge-soft {{
background: #e6f0ff;
color: #1d4ed8;
border-radius: 999px;
padding: 6px 10px;
font-weight: 700;
}}
</style>
</head>
<body>
<div class="container py-4">
<div class="top-card mb-4">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<h3 class="mb-1"><i class="fas fa-user-graduate me-2"></i>{role_view}</h3>
<div class="opacity-75">Ученик: <b>{student_username}</b> | Класс: <b>{class_name}</b></div>
</div>
<div class="d-flex gap-2">
<a href="{back_link}" class="btn btn-light btn-sm"><i class="fas fa-arrow-left me-1"></i>Назад</a>
<a href="/logout" class="btn btn-outline-light btn-sm">Выход</a>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-md-4">
<div class="metric">
<div class="text-muted">Средний балл</div>
<h3 class="mb-0">{stats['avg']}</h3>
<small class="text-muted">Оценок: {stats['grade_count']}</small>
</div>
</div>
<div class="col-md-4">
<div class="metric">
<div class="text-muted">Деньги за оценки (всего)</div>
<h3 class="mb-0 {net_color}">{'+' if net_money >= 0 else ''}{net_money}</h3>
<small class="text-success me-2">Получено: +{positive_total}</small>
<small class="text-danger">Списано: -{negative_total}</small>
</div>
</div>
<div class="col-md-4">
<div class="metric">
<div class="text-muted">Текущий баланс счета</div>
<h3 class="mb-0">{stats['current_balance']}</h3>
<small class="text-muted">Банковский баланс аккаунта</small>
</div>
</div>
</div>
<div class="table-card">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0"><i class="fas fa-chart-line me-2"></i>Графики</h5>
<span class="badge-soft">Динамика оценок и денег</span>
</div>
<div class="row g-4">
<div class="col-lg-6"><canvas id="gradesChart" height="180"></canvas></div>
<div class="col-lg-6"><canvas id="moneyChart" height="180"></canvas></div>
</div>
</div>
<div class="row g-3">
<div class="col-lg-5">
<div class="table-card">
<h5><i class="fas fa-book me-2"></i>Оценки по предметам</h5>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead><tr><th>Предмет</th><th>Оценки</th><th>Средний</th></tr></thead>
<tbody>{subject_rows}</tbody>
</table>
</div>
</div>
</div>
<div class="col-lg-7">
<div class="table-card">
<h5><i class="fas fa-history me-2"></i>История начислений за оценки</h5>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead>
<tr><th>#</th><th>Дата</th><th>Предмет</th><th>Оценка</th><th>Изменение</th><th>Итог</th></tr>
</thead>
<tbody>{timeline_rows}</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const labels = {json.dumps(labels)};
const grades = {json.dumps(grades_series)};
const avgGrades = {json.dumps(avg_series)};
const money = {json.dumps(money_series)};
new Chart(document.getElementById('gradesChart'), {{
type: 'line',
data: {{
labels,
datasets: [
{{ label: 'Оценка', data: grades, borderColor: '#2563eb', tension: 0.25 }},
{{ label: 'Средний балл', data: avgGrades, borderColor: '#16a34a', tension: 0.25 }}
]
}},
options: {{ scales: {{ y: {{ min: 2, max: 5, ticks: {{ stepSize: 1 }} }} }} }}
}});
new Chart(document.getElementById('moneyChart'), {{
type: 'line',
data: {{
labels,
datasets: [{{ label: 'Деньги за оценки', data: money, borderColor: '#f59e0b', tension: 0.25 }}]
}}
}});
</script>
</body>
</html>
"""
def render_parent_panel(parent_username):
users = load_users()
if parent_username not in users:
return "Родитель не найден", 404
children = get_parent_children(parent_username)
class_map = load_classes()
journal = load_journal()
children_rows = ""
for child in children:
if child not in users:
continue
stats = get_student_stats(child)
class_name = get_student_class(child)
net = stats["finance_delta"]
net_text = f"+{net}" if net >= 0 else str(net)
net_class = "text-success" if net >= 0 else "text-danger"
children_rows += f"""
<tr>
<td><b>{child}</b></td>
<td>{class_name}</td>
<td>{stats['avg']}</td>
<td>{stats['grade_count']}</td>
<td class="{net_class}"><b>{net_text}</b></td>
<td>
<a href="/parent/student/{child}" class="btn btn-sm btn-outline-primary">Открыть</a>
<form action="/parent/remove_child/{child}" method="post" style="display:inline;">
<button class="btn btn-sm btn-outline-danger">Удалить</button>
</form>
</td>
</tr>
"""
if not children_rows:
children_rows = "<tr><td colspan='6' class='text-center text-muted'>Пока не добавлены дети</td></tr>"
# Кандидаты для быстрого добавления ребенка
linked = set(children)
student_candidates = []
for username, user_data in users.items():
if not isinstance(user_data, dict):
continue
role = get_user_role(user_data)
in_classes = any(username in students for students in class_map.values())
in_journal = username in journal
if role == ROLE_STUDENT or in_classes or in_journal:
if username not in linked and username != parent_username:
student_candidates.append(username)
student_candidates = sorted(set(student_candidates))
options = '<option value="" selected disabled>Выберите ученика</option>'
for student_name in student_candidates:
options += f'<option value="{student_name}">{student_name}</option>'
if not student_candidates:
options = '<option value="" selected disabled>Нет доступных учеников</option>'
return f"""
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Панель родителя</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {{ background: #f5f8ff; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }}
.hero {{ background: linear-gradient(135deg, #0f766e, #0ea5e9); color: #fff; border-radius: 16px; padding: 22px; }}
.cardx {{ background: #fff; border-radius: 14px; border: 1px solid #e5ecff; box-shadow: 0 6px 16px rgba(41,72,150,0.12); }}
</style>
</head>
<body>
<div class="container py-4">
<div class="hero mb-3">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<h3 class="mb-1">Панель родителя</h3>
<div class="opacity-75">Профиль: <b>{parent_username}</b></div>
</div>
<div class="d-flex gap-2">
<a href="/dashboard" class="btn btn-light btn-sm">Назад</a>
<a href="/logout" class="btn btn-outline-light btn-sm">Выход</a>
</div>
</div>
</div>
<div class="cardx p-3 mb-3">
<h5>Добавить ребенка по логину</h5>
<form method="post" action="/parent/add_child" class="row g-2">
<div class="col-md-8">
<select name="student" class="form-select" required>
{options}
</select>
</div>
<div class="col-md-4">
<button class="btn btn-primary w-100">Добавить</button>
</div>
</form>
</div>
<div class="cardx p-3">
<h5>Дети и их успеваемость</h5>
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Ученик</th><th>Класс</th><th>Средний</th><th>Оценок</th><th>Деньги за оценки</th><th>Действия</th>
</tr>
</thead>
<tbody>
{children_rows}
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
"""
def render_manager_panel(manager_username):
users = load_users()
payments = load_payments()
journal = load_journal()
classes = load_classes()
history = load_history()
shop_data = load_shop()
valid_users = {u: d for u, d in users.items() if isinstance(d, dict)}
role_counts = {role: 0 for role in sorted(VALID_ROLES)}
total_balance = 0
for _, user_data in valid_users.items():
role_counts[get_user_role(user_data)] += 1
total_balance += int(user_data.get("balance", 0))
payment_total = len(payments)
payment_pending = len([p for p in payments if p.get("status") == "pending"])
payment_completed = len([p for p in payments if p.get("status") == "completed"])
recent_payments_rows = ""
for payment in sorted(payments, key=lambda x: x.get("created", 0), reverse=True)[:20]:
created = datetime.fromtimestamp(payment.get("created", 0)).strftime("%d.%m.%Y %H:%M")
recent_payments_rows += f"""
<tr>
<td>{payment.get('id')}</td>
<td>{payment.get('payer')}</td>
<td>{payment.get('receiver')}</td>
<td>{payment.get('amount')}</td>
<td>{payment.get('status')}</td>
<td>{created}</td>
</tr>
"""
if not recent_payments_rows:
recent_payments_rows = "<tr><td colspan='6' class='text-center text-muted'>Нет платежей</td></tr>"
students_set = set(journal.keys())
for _, students in classes.items():
students_set.update(students)
for username, user_data in valid_users.items():
if get_user_role(user_data) == ROLE_STUDENT:
students_set.add(username)
top_students = []
for student in students_set:
stats = get_student_stats(student)
top_students.append({
"student": student,
"avg": stats["avg"],
"grades": stats["grade_count"],
"finance": stats["finance_delta"],
"class_name": get_student_class(student)
})
top_students.sort(key=lambda x: (x["avg"], x["grades"], x["finance"]), reverse=True)
top_students_rows = ""
for i, item in enumerate(top_students[:10], start=1):
top_students_rows += f"""
<tr>
<td>{i}</td>
<td><a href="/manager/student/{item['student']}">{item['student']}</a></td>
<td>{item['class_name']}</td>
<td>{item['avg']}</td>
<td>{item['grades']}</td>
<td>{item['finance']}</td>
</tr>
"""
if not top_students_rows:
top_students_rows = "<tr><td colspan='6' class='text-center text-muted'>Нет данных об учениках</td></tr>"
role_rows = ""
for role_name in sorted(role_counts.keys()):
role_rows += f"<tr><td>{role_name}</td><td>{role_counts[role_name]}</td></tr>"
recent_ops_rows = ""
for op in sorted(history, key=lambda x: x.get("time", 0), reverse=True)[:15]:
op_time = datetime.fromtimestamp(op.get("time", 0)).strftime("%d.%m.%Y %H:%M")
recent_ops_rows += f"""
<tr>
<td>{op_time}</td>
<td>{op.get('user')}</td>
<td>{op.get('action')}</td>
<td>{op.get('amount')}</td>
<td>{op.get('target', '-')}</td>
</tr>
"""
if not recent_ops_rows:
recent_ops_rows = "<tr><td colspan='5' class='text-center text-muted'>Нет операций</td></tr>"
return f"""
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Панель менеджера</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {{ background: #f5f7fb; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }}
.hero {{ background: linear-gradient(135deg, #7c3aed, #2563eb); color: #fff; border-radius: 16px; padding: 22px; }}
.metric {{ background: #fff; border-radius: 14px; border: 1px solid #e6ebff; box-shadow: 0 6px 16px rgba(50,70,150,0.12); padding: 16px; height: 100%; }}
.cardx {{ background: #fff; border-radius: 14px; border: 1px solid #e6ebff; box-shadow: 0 6px 16px rgba(50,70,150,0.12); padding: 16px; margin-top: 14px; }}
</style>
</head>
<body>
<div class="container py-4">
<div class="hero mb-3">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<h3 class="mb-1">Панель менеджера</h3>
<div class="opacity-75">Пользователь: <b>{manager_username}</b></div>
</div>
<div class="d-flex gap-2">
<a href="/dashboard" class="btn btn-light btn-sm">Назад</a>
<a href="/logout" class="btn btn-outline-light btn-sm">Выход</a>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-md-3"><div class="metric"><div class="text-muted">Пользователи</div><h3>{len(valid_users)}</h3></div></div>
<div class="col-md-3"><div class="metric"><div class="text-muted">Общий баланс</div><h3>{total_balance}</h3></div></div>
<div class="col-md-3"><div class="metric"><div class="text-muted">Платежей всего</div><h3>{payment_total}</h3></div></div>
<div class="col-md-3"><div class="metric"><div class="text-muted">Платежей pending/completed</div><h3>{payment_pending}/{payment_completed}</h3></div></div>
</div>
<div class="row g-3">
<div class="col-lg-4">
<div class="cardx">
<h5>Роли в системе</h5>
<table class="table table-sm">
<thead><tr><th>Роль</th><th>Количество</th></tr></thead>
<tbody>{role_rows}</tbody>
</table>
</div>
</div>
<div class="col-lg-8">
<div class="cardx">
<h5>Топ учеников</h5>
<table class="table table-sm align-middle">
<thead><tr><th>#</th><th>Ученик</th><th>Класс</th><th>Средний</th><th>Оценок</th><th>Деньги</th></tr></thead>
<tbody>{top_students_rows}</tbody>
</table>
</div>
</div>
</div>
<div class="cardx">
<h5>Последние платежи</h5>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead><tr><th>ID</th><th>Плательщик</th><th>Получатель</th><th>Сумма</th><th>Статус</th><th>Дата</th></tr></thead>
<tbody>{recent_payments_rows}</tbody>
</table>
</div>
</div>
<div class="cardx">
<h5>Последние операции</h5>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead><tr><th>Дата</th><th>Пользователь</th><th>Действие</th><th>Сумма</th><th>Цель</th></tr></thead>
<tbody>{recent_ops_rows}</tbody>
</table>
</div>
</div>
<div class="text-muted small mt-2">
Товаров в магазине: {len(shop_data.get('items', []))} | Объявлений: {len(shop_data.get('ads', []))}
</div>
</div>
</body>
</html>
"""
@app.route("/teacher")
@login_required
@teacher_required
def teacher_panel():
return redirect("/journal")
@app.route("/student_panel")
@login_required
@student_required
def student_panel():
current_role = get_user_role(load_users().get(session.get("user"), {}))
student_username = session.get("user")
if current_role == ROLE_ADMIN:
requested_student = request.args.get("student", "").strip()
if requested_student:
student_username = requested_student
return render_student_panel(student_username)
@app.route("/parent_panel")
@login_required
@parent_required
def parent_panel():
users = load_users()
current_user = session.get("user")
current_role = get_user_role(users.get(current_user, {}))
parent_username = current_user
if current_role == ROLE_ADMIN:
requested_parent = request.args.get("parent", "").strip()
if requested_parent in users:
parent_username = requested_parent
return render_parent_panel(parent_username)
@app.route("/parent/add_child", methods=['POST'])
@login_required
@parent_required
def parent_add_child():
users = load_users()
links = load_parent_links()
classes = load_classes()
journal = load_journal()
current_user = session.get("user")
current_role = get_user_role(users.get(current_user, {}))
parent_username = current_user
if current_role == ROLE_ADMIN:
requested_parent = request.form.get("parent", "").strip()
if requested_parent in users:
parent_username = requested_parent
student = request.form.get("student", "").strip()
if not student:
return redirect("/parent_panel")
if student not in users:
return redirect("/parent_panel")
student_role = get_user_role(users.get(student, {}))
in_classes = any(student in students for students in classes.values())
in_journal = student in journal
if student_role != ROLE_STUDENT and not in_classes and not in_journal:
return redirect("/parent_panel")
links.setdefault(parent_username, [])
if student not in links[parent_username]:
links[parent_username].append(student)
save_parent_links(links)
ws_emit_user(parent_username, "parent.child_added", {"student": student})
ws_emit_user(student, "parent.linked", {"parent": parent_username})
ws_emit_system("parent.child_added", {
"parent": parent_username,
"student": student
})
return redirect("/parent_panel")
@app.route("/parent/remove_child/<student>", methods=['POST'])
@login_required
@parent_required
def parent_remove_child(student):
links = load_parent_links()
parent_username = session.get("user")
if parent_username in links and student in links[parent_username]:
links[parent_username] = [s for s in links[parent_username] if s != student]
save_parent_links(links)
ws_emit_user(parent_username, "parent.child_removed", {"student": student})
ws_emit_user(student, "parent.unlinked", {"parent": parent_username})
ws_emit_system("parent.child_removed", {
"parent": parent_username,
"student": student
})
return redirect("/parent_panel")
@app.route("/parent/student/<student>")
@login_required
@parent_required
def parent_student_view(student):
users = load_users()
if student not in users:
return "Ученик не найден", 404
current_user = session.get("user")
current_role = get_user_role(users.get(current_user, {}))
if current_role != ROLE_ADMIN:
children = get_parent_children(current_user)
if student not in children:
return redirect("/parent_panel")
return render_student_panel(student)
@app.route("/manager_panel")
@login_required
@manager_required
def manager_panel():
return render_manager_panel(session.get("user"))
@app.route("/manager/student/<student>")
@login_required
@manager_required
def manager_student_view(student):
users = load_users()
if student not in users:
return "Ученик не найден", 404
return render_student_panel(student)
@app.route("/add_subject", methods=['POST'])
@login_required
@teacher_required
def add_subject():
subject = request.form.get("subject", "").strip()
if not subject:
return redirect("/journal")
subjects = load_subjects()
if subject not in subjects:
subjects.append(subject)
subjects.sort()
save_subjects(subjects)
ws_emit_system("journal.subject_added", {"subject": subject, "by": session.get("user")})
return redirect("/journal")
@app.route("/add_class", methods=['POST'])
@login_required
@teacher_required
def add_class():
class_name = request.form.get("class_name", "").strip()
if not class_name:
return redirect("/journal")
classes = load_classes()
is_new_class = class_name not in classes
classes.setdefault(class_name, [])
save_classes(classes)
if is_new_class:
ws_emit_system("journal.class_added", {"class_name": class_name, "by": session.get("user")})
return redirect("/journal")
@app.route("/add_student_to_class", methods=['POST'])
@login_required
@teacher_required
def add_student_to_class():
student = request.form.get("student", "").strip()
class_name = request.form.get("class_name", "").strip()
if not student or not class_name:
return redirect("/journal")
users = load_users()
if student not in users:
return "Ученик не найден", 404
classes = load_classes()
classes.setdefault(class_name, [])
if student not in classes[class_name]:
classes[class_name].append(student)
save_classes(classes)
ws_emit_system("journal.student_added_to_class", {
"student": student,
"class_name": class_name,
"by": session.get("user")
})
return redirect("/journal")
@app.route("/add_grade", methods=['POST'])
@login_required
@teacher_required
def add_grade():
student = request.form.get("student", "").strip()
subject = request.form.get("subject", "").strip()
grade_raw = request.form.get("grade", "").strip()
try:
grade = int(grade_raw)
except ValueError:
return "Оценка должна быть числом от 2 до 5", 400
if grade not in [2, 3, 4, 5]:
return "Оценка должна быть от 2 до 5", 400
users = load_users()
if student not in users:
return "Ученик не найден", 404
if not subject:
return "Укажите предмет", 400
journal = load_journal()
journal.setdefault(student, {})
journal[student].setdefault(subject, [])
journal[student][subject].append(grade)
save_journal(journal)
delta = calculate_money_for_grade(grade)
users[student]["balance"] = users[student].get("balance", 0) + delta
save_users(users)
sign = "+" if delta >= 0 else ""
add_history(student, "grade_update", abs(delta), f"{subject}: оценка {grade} ({sign}{delta})")
ws_emit_system("journal.grade_added", {
"student": student,
"subject": subject,
"grade": grade,
"delta": delta,
"by": session.get("user")
})
ws_emit_balance_update(student)
return redirect("/journal")
@app.route("/student_graph/<student>")
@login_required
@teacher_required
def student_graph(student):
stats = get_student_stats(student)
all_grades = []
for grades in stats["subjects"].values():
all_grades.extend(grades)
labels = list(range(1, len(all_grades) + 1))
return f"""
<h2>График успеваемости: {student}</h2>
<a href="/journal">Назад</a>
<canvas id="chart" width="900" height="320"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const labels = {json.dumps(labels)};
const grades = {json.dumps(all_grades)};
new Chart(document.getElementById('chart'), {{
type: 'line',
data: {{
labels: labels,
datasets: [{{ label: 'Оценки', data: grades, borderColor: '#0d6efd', tension: 0.25 }}]
}},
options: {{ scales: {{ y: {{ min: 2, max: 5, ticks: {{ stepSize: 1 }} }} }} }}
}});
</script>
"""
@app.route("/student_avg_graph/<student>")
@login_required
@teacher_required
def student_avg_graph(student):
stats = get_student_stats(student)
all_grades = []
for grades in stats["subjects"].values():
all_grades.extend(grades)
averages = []
current = []
for g in all_grades:
current.append(g)
averages.append(round(sum(current) / len(current), 2))
labels = list(range(1, len(averages) + 1))
return f"""
<h2>Динамика среднего балла: {student}</h2>
<a href="/journal">Назад</a>
<canvas id="chart" width="900" height="320"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const labels = {json.dumps(labels)};
const dataPoints = {json.dumps(averages)};
new Chart(document.getElementById('chart'), {{
type: 'line',
data: {{
labels: labels,
datasets: [{{ label: 'Средний балл', data: dataPoints, borderColor: '#198754', tension: 0.25 }}]
}},
options: {{ scales: {{ y: {{ min: 2, max: 5 }} }} }}
}});
</script>
"""
@app.route("/student_finance_graph/<student>")
@login_required
@teacher_required
def student_finance_graph(student):
stats = get_student_stats(student)
all_grades = []
for grades in stats["subjects"].values():
all_grades.extend(grades)
balance_history = []
balance = 0
for g in all_grades:
balance += calculate_money_for_grade(g)
balance_history.append(balance)
labels = list(range(1, len(balance_history) + 1))
return f"""
<h2>Финансовый график ученика: {student}</h2>
<a href="/journal">Назад</a>
<canvas id="chart" width="900" height="320"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const labels = {json.dumps(labels)};
const dataPoints = {json.dumps(balance_history)};
new Chart(document.getElementById('chart'), {{
type: 'line',
data: {{
labels: labels,
datasets: [{{ label: 'Баланс от оценок', data: dataPoints, borderColor: '#fd7e14', tension: 0.25 }}]
}}
}});
</script>
"""
@app.route("/student_stats/<student>")
@login_required
@teacher_required
def student_stats(student):
users = load_users()
if student not in users:
return "Ученик не найден", 404
stats = get_student_stats(student)
class_name = get_student_class(student)
subjects_rows = ""
for subject, grades in stats["subjects"].items():
sub_avg = round(sum(grades) / len(grades), 2) if grades else 0
subjects_rows += f"<tr><td>{subject}</td><td>{', '.join(map(str, grades))}</td><td>{sub_avg}</td></tr>"
if not subjects_rows:
subjects_rows = "<tr><td colspan='3'>Нет данных</td></tr>"
return f"""
<h2>Статистика ученика: {student}</h2>
<a href="/journal">Назад</a>
<p>Класс: <b>{class_name}</b></p>
<p>Средний балл: <b>{stats['avg']}</b></p>
<p>Количество оценок: <b>{stats['grade_count']}</b></p>
<p>Финансовый итог по оценкам: <b>{stats['finance_delta']}</b></p>
<p>Текущий баланс: <b>{stats['current_balance']}</b></p>
<table border="1" cellpadding="8">
<tr><th>Предмет</th><th>Оценки</th><th>Средний</th></tr>
{subjects_rows}
</table>
"""
@app.route("/rating")
@login_required
@teacher_required
def rating():
journal = load_journal()
users = load_users()
result = []
for student in journal.keys():
stats = get_student_stats(student)
result.append({
"student": student,
"avg": stats["avg"],
"balance": users.get(student, {}).get("balance", 0),
"class_name": get_student_class(student)
})
result.sort(key=lambda x: (x["avg"], x["balance"]), reverse=True)
rows = ""
place = 1
for item in result:
rows += f"""
<tr>
<td>{place}</td>
<td>{item['student']}</td>
<td>{item['class_name']}</td>
<td>{item['avg']}</td>
<td>{item['balance']}</td>
</tr>
"""
place += 1
if not rows:
rows = "<tr><td colspan='5'>Нет данных для рейтинга</td></tr>"
return f"""
<h2>Рейтинг учеников</h2>
<a href="/journal">Назад</a>
<table border="1" cellpadding="8">
<tr>
<th>Место</th>
<th>Ученик</th>
<th>Класс</th>
<th>Средний балл</th>
<th>Баланс</th>
</tr>
{rows}
</table>
"""
@app.route("/journal")
@login_required
@teacher_required
def journal_page():
journal = load_journal()
classes = load_classes()
subjects = load_subjects()
users = load_users()
students_rows = ""
for student in sorted(journal.keys()):
stats = get_student_stats(student)
class_name = get_student_class(student)
balance_class = "pos" if stats["current_balance"] >= 0 else "neg"
students_rows += f"""
<tr>
<td><b>{student}</b></td>
<td><span class="class-pill">{class_name}</span></td>
<td>{stats['avg']}</td>
<td><span class="balance {balance_class}">{stats['current_balance']}</span></td>
<td class="actions">
<a class="link-btn" href="/student_graph/{student}">Успеваемость</a>
<a class="link-btn" href="/student_avg_graph/{student}">Средний</a>
<a class="link-btn" href="/student_finance_graph/{student}">Финансы</a>
<a class="link-btn" href="/student_stats/{student}">Статистика</a>
</td>
</tr>
"""
if not students_rows:
students_rows = "<tr><td colspan='5' class='empty'>Пока нет данных по ученикам</td></tr>"
class_rows = ""
for class_name, students in sorted(classes.items()):
student_chips = ", ".join([f"<span class='student-chip'>{s}</span>" for s in students]) if students else "<span class='muted'>—</span>"
class_rows += f"<tr><td><span class='class-pill'>{class_name}</span></td><td>{student_chips}</td></tr>"
if not class_rows:
class_rows = "<tr><td colspan='2' class='empty'>Классы не добавлены</td></tr>"
subjects_text = "".join([f"<span class='subject-chip'>{s}</span>" for s in subjects]) if subjects else "<span class='muted'>Предметы не добавлены</span>"
student_candidates = set(journal.keys())
for class_students in classes.values():
student_candidates.update(class_students)
for username, user_data in users.items():
if not isinstance(user_data, dict):
continue
role = user_data.get("role", "client")
if role in [ROLE_ADMIN, ROLE_TEACHER, ROLE_MANAGER, ROLE_PARENT]:
continue
student_candidates.add(username)
grade_students = sorted(student_candidates)
subject_candidates = set(subjects)
for student_subjects in journal.values():
if isinstance(student_subjects, dict):
subject_candidates.update(student_subjects.keys())
grade_subjects = sorted(subject_candidates)
if grade_students:
student_options = '<option value="" selected disabled>Выберите ученика</option>' + "".join(
[f'<option value="{student}">{student}</option>' for student in grade_students]
)
else:
student_options = '<option value="" selected disabled>Сначала добавьте учеников</option>'
if grade_subjects:
subject_options = '<option value="" selected disabled>Выберите предмет</option>' + "".join(
[f'<option value="{subject}">{subject}</option>' for subject in grade_subjects]
)
else:
subject_options = '<option value="" selected disabled>Сначала добавьте предметы</option>'
grade_options = """
<option value="" selected disabled>Выберите оценку</option>
<option value="5">5</option>
<option value="4">4</option>
<option value="3">3</option>
<option value="2">2</option>
"""
grade_submit_disabled = "disabled" if not grade_students or not grade_subjects else ""
return f"""
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Учительская панель</title>
<style>
:root {{
--primary: #4361ee;
--secondary: #3a0ca3;
--bg: #f4f7fc;
--surface: #ffffff;
--line: #e6ebf5;
--text: #1f2a44;
--muted: #6b7894;
--success: #168a5a;
--danger: #c63b3b;
--shadow: 0 12px 28px rgba(37, 72, 160, 0.12);
}}
* {{
box-sizing: border-box;
}}
body {{
margin: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: radial-gradient(circle at top left, #e9efff 0%, var(--bg) 45%);
color: var(--text);
}}
.journal-shell {{
max-width: 1220px;
margin: 28px auto;
padding: 0 16px 28px;
}}
.header-card {{
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
color: #fff;
border-radius: 18px;
padding: 20px;
box-shadow: var(--shadow);
margin-bottom: 18px;
}}
.top-links {{
margin-top: 12px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}}
.top-links a {{
color: #fff;
background: rgba(255, 255, 255, 0.14);
text-decoration: none;
padding: 8px 14px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.18);
}}
.forms-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 14px;
margin-bottom: 16px;
}}
.panel {{
background: var(--surface);
border-radius: 14px;
border: 1px solid var(--line);
box-shadow: 0 6px 20px rgba(30, 60, 135, 0.08);
padding: 16px;
}}
.panel h3 {{
margin: 0 0 10px;
font-size: 18px;
}}
.panel form {{
display: flex;
gap: 8px;
flex-wrap: wrap;
}}
.field {{
flex: 1 1 160px;
border: 1px solid #d7deee;
border-radius: 10px;
padding: 10px 12px;
font-size: 14px;
}}
.btn {{
border: none;
border-radius: 10px;
padding: 10px 14px;
color: #fff;
cursor: pointer;
background: linear-gradient(135deg, var(--primary), var(--secondary));
font-weight: 600;
}}
.chips {{
margin-top: 10px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}}
.subject-chip {{
background: #e8eeff;
color: #2e4cae;
border-radius: 999px;
padding: 5px 10px;
font-size: 13px;
font-weight: 600;
}}
.rule {{
background: #fff8e8;
border: 1px solid #fde1aa;
color: #7a570d;
border-radius: 12px;
padding: 10px 12px;
margin: 0 0 16px;
font-weight: 600;
}}
.table-card {{
background: var(--surface);
border: 1px solid var(--line);
border-radius: 14px;
box-shadow: 0 6px 20px rgba(30, 60, 135, 0.08);
padding: 16px;
margin-bottom: 14px;
overflow: auto;
}}
.table-card h3 {{
margin: 0 0 12px;
}}
table {{
width: 100%;
border-collapse: collapse;
min-width: 690px;
}}
th {{
text-align: left;
background: #f0f4ff;
color: #304478;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.4px;
}}
th, td {{
border-bottom: 1px solid var(--line);
padding: 10px 12px;
vertical-align: top;
}}
.class-pill {{
background: #eef2ff;
color: #2a4597;
border: 1px solid #d9e2ff;
border-radius: 999px;
padding: 4px 10px;
font-weight: 700;
font-size: 13px;
}}
.student-chip {{
background: #f2f5fd;
border: 1px solid #dbe3f7;
border-radius: 999px;
padding: 4px 9px;
font-size: 13px;
white-space: nowrap;
}}
.muted {{
color: var(--muted);
}}
.balance.pos {{
color: var(--success);
font-weight: 700;
}}
.balance.neg {{
color: var(--danger);
font-weight: 700;
}}
.actions {{
display: flex;
flex-wrap: wrap;
gap: 6px;
}}
.link-btn {{
text-decoration: none;
color: #2949a1;
background: #edf2ff;
border: 1px solid #d8e2ff;
padding: 5px 10px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
}}
.empty {{
color: var(--muted);
text-align: center;
padding: 16px;
}}
@media (max-width: 768px) {{
.header-card {{
border-radius: 14px;
}}
.panel form {{
flex-direction: column;
}}
.btn {{
width: 100%;
}}
}}
</style>
</head>
<body>
<div class="journal-shell">
<div class="header-card">
<h2>Учительская панель: электронный дневник</h2>
<div class="top-links">
<a href="/dashboard">Назад в банк</a>
<a href="/rating">Рейтинг учеников</a>
</div>
</div>
<div class="forms-grid">
<div class="panel">
<h3>Добавить предмет</h3>
<form method="post" action="/add_subject">
<input class="field" name="subject" placeholder="Математика" required>
<button class="btn">Добавить предмет</button>
</form>
<div class="chips">{subjects_text}</div>
</div>
<div class="panel">
<h3>Добавить класс</h3>
<form method="post" action="/add_class">
<input class="field" name="class_name" placeholder="" required>
<button class="btn">Создать класс</button>
</form>
</div>
<div class="panel">
<h3>Добавить ученика в класс</h3>
<form method="post" action="/add_student_to_class">
<input class="field" name="student" placeholder="Логин ученика" required>
<input class="field" name="class_name" placeholder="Класс" required>
<button class="btn">Добавить</button>
</form>
</div>
</div>
<div class="panel" style="margin-bottom: 14px;">
<h3>Поставить оценку</h3>
<p class="rule">Финансовая логика: 5 => +20, 4 => +10, 3 => -10, 2 => -20</p>
<form method="post" action="/add_grade">
<select class="field" name="student" required>
{student_options}
</select>
<select class="field" name="subject" required>
{subject_options}
</select>
<select class="field" name="grade" required>
{grade_options}
</select>
<button class="btn" {grade_submit_disabled}>Поставить оценку</button>
</form>
</div>
<div class="table-card">
<h3>Классы</h3>
<table>
<tr><th>Класс</th><th>Ученики</th></tr>
{class_rows}
</table>
</div>
<div class="table-card">
<h3>Ученики</h3>
<table>
<tr>
<th>Ученик</th>
<th>Класс</th>
<th>Средний балл</th>
<th>Баланс</th>
<th>Инструменты</th>
</tr>
{students_rows}
</table>
</div>
</div>
</body>
</html>
"""
if __name__ == '__main__':
import threading
def daily_update_thread():
while True:
time.sleep(86400)
daily_update()
thread = threading.Thread(target=daily_update_thread, daemon=True)
thread.start()
requested_debug = parse_bool(os.environ.get("BANK_DEBUG", "1"))
app.run(host='0.0.0.0', port=5000, debug=requested_debug, use_reloader=requested_debug)