3754 lines
148 KiB
Python
3754 lines
148 KiB
Python
from flask import Flask, request, redirect, session, Response, jsonify
|
|
import json, os, random, time, uuid
|
|
from datetime import datetime
|
|
from functools import wraps
|
|
|
|
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"
|
|
CARDS_FILE = "cards.json"
|
|
SHOP_FILE = "shop.json"
|
|
PAYMENTS_FILE = "payments.json" # Новый файл для платежей
|
|
|
|
# ---------- ИНИЦИАЛИЗАЦИЯ ФАЙЛОВ ----------
|
|
def init_files():
|
|
"""Инициализация файлов (без удаления существующих)"""
|
|
# Создаем файлы если они не существуют
|
|
files_to_init = [
|
|
(USERS_FILE, {
|
|
"admin": {
|
|
"password": "admin123",
|
|
"balance": 100000,
|
|
"deposit": 0,
|
|
"credit": 0,
|
|
"role": "admin",
|
|
"full_name": "Администратор Системы",
|
|
"email": "admin@bank.com",
|
|
"api_key": None
|
|
}
|
|
}),
|
|
(HISTORY_FILE, []),
|
|
(CARDS_FILE, {}),
|
|
(SHOP_FILE, {"items": [], "ads": []}),
|
|
(PAYMENTS_FILE, []) # Инициализация файла платежей
|
|
]
|
|
|
|
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()
|
|
|
|
# ---------- ФУНКЦИИ ДЛЯ РАБОТЫ С ДАННЫМИ ----------
|
|
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": {
|
|
"password": "admin123",
|
|
"balance": 100000,
|
|
"deposit": 0,
|
|
"credit": 0,
|
|
"role": "admin",
|
|
"full_name": "Администратор Системы",
|
|
"email": "admin@bank.com",
|
|
"api_key": None
|
|
}
|
|
}
|
|
save_users(data)
|
|
return data
|
|
# Проверяем, что все значения являются словарями
|
|
cleaned_data = {}
|
|
for username, user_data in data.items():
|
|
if isinstance(user_data, dict):
|
|
cleaned_data[username] = user_data
|
|
else:
|
|
print(f"Warning: User {username} data is not a dict, resetting")
|
|
cleaned_data[username] = {
|
|
"password": "reset123",
|
|
"balance": 1000,
|
|
"deposit": 0,
|
|
"credit": 0,
|
|
"role": "client",
|
|
"full_name": "Пользователь",
|
|
"email": "user@example.com",
|
|
"api_key": None
|
|
}
|
|
if cleaned_data != data:
|
|
save_users(cleaned_data)
|
|
return cleaned_data
|
|
except Exception as e:
|
|
print(f"Error loading users: {e}")
|
|
data = {
|
|
"admin": {
|
|
"password": "admin123",
|
|
"balance": 100000,
|
|
"deposit": 0,
|
|
"credit": 0,
|
|
"role": "admin",
|
|
"full_name": "Администратор Системы",
|
|
"email": "admin@bank.com",
|
|
"api_key": None
|
|
}
|
|
}
|
|
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_cards():
|
|
try:
|
|
with open(CARDS_FILE, 'r', encoding='utf-8') as f:
|
|
return json.load(f)
|
|
except:
|
|
return {}
|
|
|
|
def save_cards(data):
|
|
with open(CARDS_FILE, "w", encoding='utf-8') as f:
|
|
json.dump(data, f, indent=4, ensure_ascii=False)
|
|
|
|
# ---------- ФУНКЦИИ ДЛЯ ПЛАТЕЖЕЙ ----------
|
|
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 create_payment(payer, receiver, amount, description="", external_id=""):
|
|
"""Создание платежа"""
|
|
payments = load_payments()
|
|
|
|
payment_id = str(uuid.uuid4())[:12]
|
|
payment_data = {
|
|
"id": payment_id,
|
|
"payer": payer,
|
|
"receiver": receiver,
|
|
"amount": int(amount),
|
|
"description": description,
|
|
"external_id": external_id,
|
|
"created": int(time.time()),
|
|
"status": "pending", # pending, completed, failed, cancelled
|
|
"completed_at": None
|
|
}
|
|
|
|
payments.append(payment_data)
|
|
save_payments(payments)
|
|
|
|
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']})"
|
|
|
|
# Если передан payer_username, проверяем что это тот же плательщик
|
|
if payer_username and payment["payer"] != payer_username:
|
|
return False, "Неверный плательщик"
|
|
|
|
payer = payment["payer"]
|
|
receiver = payment["receiver"]
|
|
amount = payment["amount"]
|
|
|
|
# Проверяем существование пользователей
|
|
if payer not in users:
|
|
return False, "Плательщик не найден"
|
|
|
|
if receiver not in users:
|
|
return False, "Получатель не найден"
|
|
|
|
# Проверяем баланс плательщика
|
|
if users[payer]["balance"] < amount:
|
|
return False, "Недостаточно средств"
|
|
|
|
# Выполняем перевод
|
|
users[payer]["balance"] -= amount
|
|
users[receiver]["balance"] += amount
|
|
save_users(users)
|
|
|
|
# Обновляем статус платежа
|
|
for p in payments:
|
|
if p["id"] == payment_id:
|
|
p["status"] = "completed"
|
|
p["completed_at"] = int(time.time())
|
|
break
|
|
|
|
save_payments(payments)
|
|
|
|
# Добавляем в историю
|
|
add_history(payer, "payment_sent", amount, f"Платеж #{payment_id} для {receiver}")
|
|
add_history(receiver, "payment_received", amount, f"Платеж #{payment_id} от {payer}")
|
|
|
|
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)
|
|
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}")
|
|
|
|
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}")
|
|
|
|
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}")
|
|
|
|
return True, "Покупка успешно завершена"
|
|
|
|
# ---------- 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 generate_card_number(user_id, card_type="standard"):
|
|
"""Генерация номера банковской карты"""
|
|
prefixes = {
|
|
"standard": ["5469", "4276", "5375", "4856"],
|
|
"gold": ["5469", "5278", "5375"],
|
|
"platinum": ["4475", "4876", "4999"]
|
|
}
|
|
|
|
prefix = random.choice(prefixes.get(card_type, prefixes["standard"]))
|
|
middle = ''.join([str(random.randint(0, 9)) for _ in range(8)])
|
|
last_digits = ''.join([str(random.randint(0, 9)) for _ in range(4)])
|
|
|
|
card_number = f"{prefix}{middle}{last_digits}"
|
|
formatted = ' '.join([card_number[i:i+4] for i in range(0, len(card_number), 4)])
|
|
|
|
return formatted
|
|
|
|
def generate_cvv():
|
|
"""Генерация CVV кода"""
|
|
return str(random.randint(100, 999))
|
|
|
|
def generate_expiry_date():
|
|
"""Генерация срока действия карты"""
|
|
from datetime import datetime, timedelta
|
|
expiry = datetime.now() + timedelta(days=365*3)
|
|
return expiry.strftime("%m/%y")
|
|
|
|
def get_user_cards(user_id):
|
|
"""Получение всех карт пользователя"""
|
|
cards = load_cards()
|
|
return cards.get(user_id, [])
|
|
|
|
def create_user_card(user_id, user_data, card_type="standard"):
|
|
"""Создание новой карты для пользователя"""
|
|
cards = load_cards()
|
|
|
|
if user_id not in cards:
|
|
cards[user_id] = []
|
|
|
|
# Проверяем, не слишком ли много карт у пользователя
|
|
if len(cards[user_id]) >= 5:
|
|
return None
|
|
|
|
card_id = str(uuid.uuid4())[:8]
|
|
|
|
new_card = {
|
|
"id": card_id,
|
|
"number": generate_card_number(user_id, card_type),
|
|
"holder": user_data.get('full_name', user_id.upper()),
|
|
"type": card_type,
|
|
"cvv": generate_cvv(),
|
|
"expiry": generate_expiry_date(),
|
|
"balance": user_data.get('balance', 0),
|
|
"currency": "USD",
|
|
"created": int(time.time()),
|
|
"active": True,
|
|
"limit": 10000 if card_type == "standard" else 50000 if card_type == "gold" else 100000,
|
|
"color": {
|
|
"standard": "#4361ee",
|
|
"gold": "#ffd166",
|
|
"platinum": "#e5e5e5"
|
|
}.get(card_type, "#4361ee")
|
|
}
|
|
|
|
cards[user_id].append(new_card)
|
|
save_cards(cards)
|
|
|
|
# Добавляем в историю
|
|
add_history(user_id, f"create_{card_type}_card", 0, f"Card {new_card['number'][-4:]}")
|
|
|
|
return new_card
|
|
|
|
# ---------- БАНКОВСКИЕ ОПЕРАЦИИ ----------
|
|
def daily_update():
|
|
"""Ежедневное обновление депозитов и кредитов"""
|
|
users = load_users()
|
|
updated = False
|
|
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
|
|
if u.get('credit', 0) > 0:
|
|
u['credit'] = int(u['credit'] * 1.05)
|
|
updated = True
|
|
if updated:
|
|
save_users(users)
|
|
|
|
def add_history(user, action, amount, target=None):
|
|
"""Добавление записи в историю"""
|
|
hist = load_history()
|
|
hist.append({
|
|
"time": int(time.time()),
|
|
"user": user,
|
|
"action": action,
|
|
"amount": amount,
|
|
"target": target,
|
|
"id": str(uuid.uuid4())[:8]
|
|
})
|
|
save_history(hist)
|
|
|
|
# ---------- ДЕКОРАТОРЫ ----------
|
|
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 admin_required(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 user_data or not isinstance(user_data, dict) or user_data.get('role') != 'admin':
|
|
return redirect('/dashboard')
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
# ИСПРАВЛЕННЫЙ ДЕКОРАТОР 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
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
# ---------- HTML ШАБЛОНЫ ----------
|
|
def render_login():
|
|
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%);
|
|
}
|
|
body {
|
|
background: var(--gradient);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
}
|
|
.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: 30px;
|
|
text-align: center;
|
|
}
|
|
.login-body {
|
|
padding: 40px;
|
|
}
|
|
.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);
|
|
}
|
|
.alert {
|
|
border-radius: 10px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<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">Вход в систему</p>
|
|
</div>
|
|
<div class="login-body">
|
|
<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>
|
|
</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_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%);
|
|
}
|
|
body {
|
|
background: var(--gradient);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
}
|
|
.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: 30px;
|
|
text-align: center;
|
|
}
|
|
.register-body {
|
|
padding: 40px;
|
|
}
|
|
.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);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<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>
|
|
<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>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
</body>
|
|
</html>
|
|
'''
|
|
|
|
def render_card_html(card, width="400px"):
|
|
"""Генерация HTML для банковской карты"""
|
|
colors = {
|
|
"standard": "#4361ee",
|
|
"gold": "#ffd166",
|
|
"platinum": "#e5e5e5"
|
|
}
|
|
|
|
bg_color = card.get('color', colors.get(card['type'], "#4361ee"))
|
|
text_color = "#ffffff" if card['type'] == 'standard' else "#000000"
|
|
|
|
return f'''
|
|
<div class="bank-card" style="width: {width}; background: linear-gradient(135deg, {bg_color} 0%, {bg_color}dd 100%); color: {text_color};">
|
|
<div class="bank-logo">
|
|
<span>BANK CARD</span>
|
|
<div class="chip"></div>
|
|
</div>
|
|
<div class="card-number">
|
|
{card['number']}
|
|
</div>
|
|
<div class="card-holder">
|
|
{card['holder']}
|
|
</div>
|
|
</div>
|
|
'''
|
|
|
|
def render_dashboard(user_data, history_data, user_cards, shop_data=None):
|
|
"""Генерация HTML личного кабинета"""
|
|
# Форматирование времени истории
|
|
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 для банковских карт
|
|
cards_html = ""
|
|
if user_cards:
|
|
for card in user_cards:
|
|
width = "350px" if len(card['number']) < 24 else "400px" if len(card['number']) < 28 else "450px"
|
|
cards_html += f'''
|
|
<div class="col-md-6 col-lg-4">
|
|
<div class="card h-100">
|
|
<div class="card-body">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-credit-card me-2"></i>
|
|
Карта {card['type'].upper()}
|
|
</h5>
|
|
{render_card_html(card, width)}
|
|
<div class="mt-3">
|
|
<p class="mb-1"><small>CVV: <strong>{card['cvv']}</strong></small></p>
|
|
<p class="mb-1"><small>Срок: <strong>{card['expiry']}</strong></small></p>
|
|
<p class="mb-0"><small>Баланс: <strong>${card['balance']:,}</strong></small></p>
|
|
<p class="mb-0"><small>Лимит: <strong>${card['limit']:,}</strong></small></p>
|
|
<form action="/delete_card/{card['id']}" method="post" class="mt-2">
|
|
<button type="submit" class="btn btn-danger btn-sm w-100">
|
|
<i class="fas fa-trash me-1"></i> Удалить
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
'''
|
|
else:
|
|
cards_html = '''
|
|
<div class="col-12">
|
|
<div class="alert alert-info">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
У вас еще нет банковских карт. Создайте свою первую карту!
|
|
</div>
|
|
</div>
|
|
'''
|
|
|
|
# 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>
|
|
'''
|
|
|
|
# HTML для админ-панели
|
|
admin_tab = ""
|
|
if user_data.get('role') == 'admin':
|
|
admin_tab = '''
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="/admin">
|
|
<i class="fas fa-cogs me-2"></i>Админ-панель
|
|
</a>
|
|
</li>
|
|
'''
|
|
|
|
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;
|
|
}}
|
|
|
|
.bank-card {{
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
color: white;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
|
|
position: relative;
|
|
overflow: hidden;
|
|
min-height: 180px;
|
|
margin: 10px 0;
|
|
}}
|
|
|
|
.bank-card::before {{
|
|
content: "";
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: radial-gradient(circle at top right, rgba(255,255,255,0.1) 0%, transparent 50%);
|
|
}}
|
|
|
|
.bank-logo {{
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
letter-spacing: 1px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
}}
|
|
|
|
.chip {{
|
|
width: 35px;
|
|
height: 25px;
|
|
background: linear-gradient(135deg, #ffd166 0%, #ff9e00 100%);
|
|
border-radius: 5px;
|
|
position: relative;
|
|
}}
|
|
|
|
.chip::before {{
|
|
content: "";
|
|
position: absolute;
|
|
top: 3px;
|
|
left: 3px;
|
|
right: 3px;
|
|
bottom: 3px;
|
|
border: 2px solid rgba(0,0,0,0.2);
|
|
border-radius: 3px;
|
|
}}
|
|
|
|
.card-number {{
|
|
font-size: 18px;
|
|
letter-spacing: 2px;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
margin: 15px 0;
|
|
word-break: break-all;
|
|
}}
|
|
|
|
.card-holder {{
|
|
font-size: 14px;
|
|
letter-spacing: 1px;
|
|
text-transform: uppercase;
|
|
}}
|
|
|
|
.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;
|
|
}}
|
|
|
|
.create-card-btn {{
|
|
background: linear-gradient(135deg, #ffd166 0%, #ff9e00 100%);
|
|
border: none;
|
|
color: #000;
|
|
font-weight: 600;
|
|
}}
|
|
|
|
.create-card-btn:hover {{
|
|
background: linear-gradient(135deg, #ffc145 0%, #e68900 100%);
|
|
color: #000;
|
|
}}
|
|
|
|
.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);
|
|
}}
|
|
</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']} ({user_data.get('role', 'client')})
|
|
</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>
|
|
<button class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#creditModal">
|
|
<i class="fas fa-hand-holding-usd me-1"></i>Взять кредит
|
|
</button>
|
|
<button class="btn btn-outline-success" data-bs-toggle="modal" data-bs-target="#transferModal">
|
|
<i class="fas fa-exchange-alt me-1"></i>Перевод
|
|
</button>
|
|
<button class="btn create-card-btn" data-bs-toggle="modal" data-bs-target="#cardModal">
|
|
<i class="fas fa-credit-card me-1"></i>Создать карту
|
|
</button>
|
|
<a href="/payment_page" class="btn btn-warning">
|
|
<i class="fas fa-credit-card me-1"></i>Оплата
|
|
</a>
|
|
</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="#cards">
|
|
<i class="fas fa-credit-card 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>
|
|
<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>
|
|
{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-credit-card text-warning 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>
|
|
<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>
|
|
|
|
<div class="tab-pane fade" id="cards">
|
|
<div class="row g-4">
|
|
{cards_html}
|
|
</div>
|
|
</div>
|
|
|
|
{shop_tab_content}
|
|
|
|
<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-dollar-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-info">GET</span></td>
|
|
<td>/api/v1/cards</td>
|
|
<td>Получить список карт</td>
|
|
<td>-</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-success">POST</span></td>
|
|
<td>/api/v1/cards/create</td>
|
|
<td>Создать карту</td>
|
|
<td>card_type (standard|gold|platinum)</td>
|
|
</tr>
|
|
<tr>
|
|
<td><span class="badge bg-danger">POST</span></td>
|
|
<td>/api/v1/cards/delete</td>
|
|
<td>Удалить карту</td>
|
|
<td>card_id (str)</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-success">POST</span></td>
|
|
<td>/api/v1/payment/create</td>
|
|
<td>Создать платеж</td>
|
|
<td>receiver, amount, description</td>
|
|
</tr>
|
|
<tr>
|
|
<td><span class="badge bg-success">POST</span></td>
|
|
<td>/api/v1/payment/process</td>
|
|
<td>Выполнить платеж</td>
|
|
<td>payment_id</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="cardModal" 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="/create_card" method="post">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Тип карты</label>
|
|
<select name="card_type" class="form-select">
|
|
<option value="standard">Стандартная (бесплатно)</option>
|
|
<option value="gold">Золотая ($50)</option>
|
|
<option value="platinum">Платиновая ($100)</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="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);
|
|
|
|
// Валидация форм
|
|
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("Пожалуйста, введите корректное значение");
|
|
}}
|
|
}});
|
|
}});
|
|
}});
|
|
|
|
// Анимация карт
|
|
document.querySelectorAll(".bank-card").forEach(card => {{
|
|
card.addEventListener("mouseenter", function() {{
|
|
this.style.transform = "translateY(-5px)";
|
|
this.style.boxShadow = "0 15px 35px rgba(0,0,0,0.3)";
|
|
}});
|
|
|
|
card.addEventListener("mouseleave", function() {{
|
|
this.style.transform = "translateY(0)";
|
|
this.style.boxShadow = "0 10px 25px rgba(0,0,0,0.2)";
|
|
}});
|
|
}});
|
|
|
|
// Копирование API ключа
|
|
function copyApiKey() {{
|
|
const apiKeyInput = document.getElementById("apiKey");
|
|
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 = user.get('role', 'client')
|
|
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 = "success" if role == 'admin' else "primary" if role == 'manager' else "secondary"
|
|
|
|
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="client" {'selected' if role == 'client' else ''}>Клиент</option>
|
|
<option value="admin" {'selected' if role == 'admin' else ''}>Админ</option>
|
|
<option value="manager" {'selected' if role == 'manager' 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():
|
|
"""Страница оплаты"""
|
|
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%);
|
|
}
|
|
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;
|
|
}
|
|
.qr-code {
|
|
width: 200px;
|
|
height: 200px;
|
|
background: #f8f9fa;
|
|
border-radius: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin: 20px auto;
|
|
border: 1px solid #dee2e6;
|
|
}
|
|
.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-8">
|
|
<div class="payment-card">
|
|
<div class="payment-header">
|
|
<h3><i class="fas fa-credit-card me-2"></i>Платежная система</h3>
|
|
<p class="mb-0">Принимайте платежи от клиентов через ссылки или API</p>
|
|
</div>
|
|
<div class="payment-body">
|
|
<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
|
|
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 class="mt-4">
|
|
<h5><i class="fas fa-qrcode me-2"></i>QR-код для оплаты</h5>
|
|
<div class="qr-code" id="qrCode">
|
|
<span class="text-muted">QR-код появится после создания ссылки</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<h4><i class="fas fa-code me-2"></i>API для оплаты</h4>
|
|
<p>Используйте API для интеграции платежей в ваши приложения:</p>
|
|
|
|
<h6>1. Создать платеж:</h6>
|
|
<div class="api-example">
|
|
POST /api/v1/payment/create<br>
|
|
Headers: X-API-Key: ваш_ключ<br>
|
|
Body: {<br>
|
|
"receiver": "username",<br>
|
|
"amount": 100,<br>
|
|
"description": "Оплата за услугу"<br>
|
|
}
|
|
</div>
|
|
|
|
<h6>2. Получить статус платежа:</h6>
|
|
<div class="api-example">
|
|
GET /api/v1/payment/status/{payment_id}<br>
|
|
Headers: X-API-Key: ваш_ключ
|
|
</div>
|
|
|
|
<h6>3. Пример ссылки для оплаты:</h6>
|
|
<div class="payment-link" id="paymentLinkExample">
|
|
http://ваш-домен/pay/{payment_id}
|
|
</div>
|
|
|
|
<div class="alert alert-info mt-3">
|
|
<h6><i class="fas fa-lightbulb me-2"></i>Как это работает:</h6>
|
|
<ol class="mb-0">
|
|
<li>Создаете платеж через форму или API</li>
|
|
<li>Получаете уникальную ссылку или payment_id</li>
|
|
<li>Отправляете ссылку клиенту</li>
|
|
<li>Клиент переходит по ссылке и оплачивает</li>
|
|
<li>Вы получаете деньги на счет</li>
|
|
</ol>
|
|
</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" id="paymentsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Сумма</th>
|
|
<th>Получатель</th>
|
|
<th>Статус</th>
|
|
<th>Дата</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<!-- Данные будут загружены через JavaScript -->
|
|
</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>
|
|
// Обработка формы создания платежа
|
|
document.getElementById('paymentForm').addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
const formData = new FormData(this);
|
|
|
|
try {
|
|
const response = await fetch('/api/v1/payment/create', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
receiver: formData.get('receiver'),
|
|
amount: parseInt(formData.get('amount')),
|
|
description: formData.get('description')
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
// Обновляем пример ссылки
|
|
const paymentLink = window.location.origin + '/pay/' + result.payment_id;
|
|
document.getElementById('paymentLinkExample').textContent = paymentLink;
|
|
|
|
// Показываем QR-код (простая визуализация)
|
|
const qrCodeDiv = document.getElementById('qrCode');
|
|
qrCodeDiv.innerHTML = `
|
|
<div class="text-center">
|
|
<div style="font-size: 80px;">💰</div>
|
|
<small class="text-muted">Ссылка: ${result.payment_id}</small>
|
|
</div>
|
|
`;
|
|
|
|
alert('Платеж создан! ID: ' + result.payment_id);
|
|
|
|
// Обновляем список платежей
|
|
loadPayments();
|
|
} else {
|
|
alert('Ошибка: ' + result.error);
|
|
}
|
|
} catch (error) {
|
|
alert('Ошибка соединения: ' + error.message);
|
|
}
|
|
});
|
|
|
|
// Загрузка списка платежей
|
|
async function loadPayments() {
|
|
try {
|
|
const response = await fetch('/api/v1/payment/list');
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
const tbody = document.querySelector('#paymentsTable tbody');
|
|
tbody.innerHTML = '';
|
|
|
|
result.payments.forEach(payment => {
|
|
const row = document.createElement('tr');
|
|
|
|
// Определяем цвет статуса
|
|
let statusClass = 'warning';
|
|
if (payment.status === 'completed') statusClass = 'success';
|
|
if (payment.status === 'failed') statusClass = 'danger';
|
|
if (payment.status === 'cancelled') statusClass = 'secondary';
|
|
|
|
const date = new Date(payment.created * 1000).toLocaleString();
|
|
|
|
row.innerHTML = `
|
|
<td><small>${payment.id}</small></td>
|
|
<td><strong>$${payment.amount}</strong></td>
|
|
<td>${payment.receiver}</td>
|
|
<td><span class="badge bg-${statusClass}">${payment.status}</span></td>
|
|
<td><small>${date}</small></td>
|
|
`;
|
|
|
|
tbody.appendChild(row);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Ошибка загрузки платежей:', error);
|
|
}
|
|
}
|
|
|
|
// Загружаем платежи при загрузке страницы
|
|
document.addEventListener('DOMContentLoaded', loadPayments);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
'''
|
|
|
|
def render_pay_page(payment_id):
|
|
"""Страница оплаты для клиента"""
|
|
payments = load_payments()
|
|
|
|
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"], "Неизвестен")
|
|
|
|
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>Описание: {payment['description']}</p>
|
|
<a href="/" class="btn btn-primary mt-3">На главную</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
'''
|
|
|
|
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">
|
|
<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>{payment['description'] or 'Нет описания'}</strong>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-4">
|
|
<span>Статус:</span>
|
|
<span class="badge bg-warning">Ожидает оплаты</span>
|
|
</div>
|
|
</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>
|
|
</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
|
|
def api_balance():
|
|
"""API: Получить информацию о балансе - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
|
|
try:
|
|
user_data = request.user_data
|
|
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),
|
|
'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
|
|
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/cards', methods=['GET'])
|
|
@api_key_required
|
|
def api_cards():
|
|
"""API: Получить список карт - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
|
|
try:
|
|
cards = get_user_cards(request.username)
|
|
|
|
safe_cards = []
|
|
for card in cards:
|
|
safe_card = card.copy()
|
|
if 'number' in safe_card and len(safe_card['number']) > 4:
|
|
safe_card['number_masked'] = '**** **** **** ' + safe_card['number'][-4:]
|
|
safe_card['cvv'] = '***'
|
|
safe_cards.append(safe_card)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'count': len(safe_cards),
|
|
'cards': safe_cards
|
|
})
|
|
except Exception as e:
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
|
|
@app.route('/api/v1/deposit', methods=['POST'])
|
|
@api_key_required
|
|
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
|
|
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
|
|
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)
|
|
|
|
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
|
|
|
|
@app.route('/api/v1/cards/create', methods=['POST'])
|
|
@api_key_required
|
|
def api_create_card():
|
|
"""API: Создать карту - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
|
|
try:
|
|
if not request.is_json:
|
|
return jsonify({'success': False, 'error': 'JSON data required'}), 400
|
|
|
|
data = request.get_json()
|
|
card_type = data.get('card_type', 'standard')
|
|
|
|
if card_type not in ['standard', 'gold', 'platinum']:
|
|
return jsonify({'success': False, 'error': 'Invalid card type'}), 400
|
|
|
|
users = load_users()
|
|
user = users.get(request.username)
|
|
|
|
if not user:
|
|
return jsonify({'success': False, 'error': 'User not found'}), 404
|
|
|
|
costs = {'standard': 0, 'gold': 50, 'platinum': 100}
|
|
cost = costs.get(card_type, 0)
|
|
|
|
if user['balance'] < cost:
|
|
return jsonify({'success': False, 'error': 'Insufficient funds'}), 400
|
|
|
|
if cost > 0:
|
|
user['balance'] -= cost
|
|
save_users(users)
|
|
|
|
card = create_user_card(request.username, user, card_type)
|
|
|
|
if not card:
|
|
if cost > 0:
|
|
user['balance'] += cost
|
|
save_users(users)
|
|
return jsonify({'success': False, 'error': 'Card creation failed. Maximum cards limit reached?'}), 400
|
|
|
|
safe_card = card.copy()
|
|
if 'number' in safe_card and len(safe_card['number']) > 4:
|
|
safe_card['number_masked'] = '**** **** **** ' + safe_card['number'][-4:]
|
|
safe_card['cvv'] = '***'
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': f'{card_type.capitalize()} card created successfully',
|
|
'card': safe_card,
|
|
'cost': cost,
|
|
'new_balance': user['balance']
|
|
})
|
|
except Exception as e:
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
|
|
@app.route('/api/v1/cards/delete', methods=['POST'])
|
|
@api_key_required
|
|
def api_delete_card():
|
|
"""API: Удалить карту - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
|
|
try:
|
|
if not request.is_json:
|
|
return jsonify({'success': False, 'error': 'JSON data required'}), 400
|
|
|
|
data = request.get_json()
|
|
card_id = data.get('card_id')
|
|
|
|
if not card_id:
|
|
return jsonify({'success': False, 'error': 'Card ID required'}), 400
|
|
|
|
cards = load_cards()
|
|
user_cards = cards.get(request.username, [])
|
|
|
|
card_to_delete = None
|
|
for card in user_cards:
|
|
if card['id'] == card_id:
|
|
card_to_delete = card
|
|
break
|
|
|
|
if not card_to_delete:
|
|
return jsonify({'success': False, 'error': 'Card not found'}), 404
|
|
|
|
cards[request.username] = [c for c in user_cards if c['id'] != card_id]
|
|
save_cards(cards)
|
|
|
|
add_history(request.username, 'api_delete_card', 0, f"Card {card_to_delete['number'][-4:]}")
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': f"Card ending with {card_to_delete['number'][-4:]} deleted successfully"
|
|
})
|
|
except Exception as e:
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
|
|
# ---------- API ДЛЯ ПЛАТЕЖЕЙ ----------
|
|
|
|
@app.route('/api/v1/payment/create', methods=['POST'])
|
|
@api_key_required
|
|
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 = data.get('receiver')
|
|
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
|
|
|
|
# Создаем платеж
|
|
payment_id, payment_data = create_payment(
|
|
payer=request.username,
|
|
receiver=receiver,
|
|
amount=amount_int,
|
|
description=description,
|
|
external_id=external_id
|
|
)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Payment created successfully',
|
|
'payment_id': payment_id,
|
|
'payment': payment_data,
|
|
'payment_url': f'/pay/{payment_id}'
|
|
})
|
|
except Exception as e:
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
|
|
@app.route('/api/v1/payment/process', methods=['POST'])
|
|
@api_key_required
|
|
def api_payment_process():
|
|
"""API: Выполнить платеж"""
|
|
try:
|
|
if not request.is_json:
|
|
return jsonify({'success': False, 'error': 'JSON data required'}), 400
|
|
|
|
data = request.get_json()
|
|
payment_id = data.get('payment_id')
|
|
|
|
if not payment_id:
|
|
return jsonify({'success': False, 'error': 'Payment ID required'}), 400
|
|
|
|
success, message = process_payment(payment_id, request.username)
|
|
|
|
return jsonify({
|
|
'success': success,
|
|
'message': message
|
|
})
|
|
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
|
|
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:
|
|
users = load_users()
|
|
if users.get(request.username, {}).get('role') != 'admin':
|
|
return jsonify({'success': False, 'error': 'Access denied'}), 403
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'payment': payment
|
|
})
|
|
except Exception as e:
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
|
|
@app.route('/api/v1/payment/list', methods=['GET'])
|
|
@api_key_required
|
|
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:
|
|
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]
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'count': len(user_payments),
|
|
'payments': user_payments
|
|
})
|
|
except Exception as e:
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
|
|
@app.route('/api/v1/shop/items', methods=['GET'])
|
|
@api_key_required
|
|
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
|
|
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/system/status', methods=['GET'])
|
|
@api_key_required
|
|
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)
|
|
|
|
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
|
|
},
|
|
'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
|
|
def api_users():
|
|
"""API: Получить список пользователей (только для админов)"""
|
|
try:
|
|
users = load_users()
|
|
|
|
if not isinstance(request.user_data, dict) or request.user_data.get('role') != 'admin':
|
|
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
|
|
|
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, CARDS_FILE, SHOP_FILE, PAYMENTS_FILE]
|
|
file_status = {}
|
|
|
|
for file in files:
|
|
file_status[file] = os.path.exists(file)
|
|
|
|
return jsonify({
|
|
'status': 'healthy',
|
|
'timestamp': int(time.time()),
|
|
'files': file_status,
|
|
'message': 'System is operational'
|
|
})
|
|
except Exception as e:
|
|
return jsonify({'status': 'unhealthy', 'error': str(e)}), 500
|
|
|
|
@app.route('/api/v1/shop/create_item', methods=['POST'])
|
|
@api_key_required
|
|
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
|
|
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
|
|
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
|
|
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
|
|
def buy_item_route(item_id):
|
|
"""Покупка товара"""
|
|
success, message = buy_item(item_id, session['user'])
|
|
|
|
return redirect('/dashboard')
|
|
|
|
# ---------- ПЛАТЕЖНЫЕ МАРШРУТЫ ----------
|
|
@app.route('/payment_page')
|
|
@login_required
|
|
def payment_page():
|
|
"""Страница управления платежами"""
|
|
return render_payment_page()
|
|
|
|
@app.route('/create_payment_link', methods=['POST'])
|
|
@login_required
|
|
def create_payment_link():
|
|
"""Создание ссылки для оплаты"""
|
|
receiver = request.form.get('receiver')
|
|
amount = request.form.get('amount')
|
|
description = request.form.get('description', '')
|
|
|
|
if not receiver or not amount:
|
|
return redirect('/payment_page')
|
|
|
|
try:
|
|
amount_int = int(amount)
|
|
if amount_int <= 0:
|
|
return redirect('/payment_page')
|
|
except:
|
|
return redirect('/payment_page')
|
|
|
|
payment_id, payment_data = create_payment(
|
|
payer=session['user'],
|
|
receiver=receiver,
|
|
amount=amount_int,
|
|
description=description
|
|
)
|
|
|
|
return redirect(f'/payment_page')
|
|
|
|
@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')
|
|
password = request.form.get('password')
|
|
|
|
if not username or not password:
|
|
return "Неверные данные", 400
|
|
|
|
# Проверяем логин и пароль
|
|
users = load_users()
|
|
user_data = users.get(username)
|
|
|
|
if not user_data or user_data['password'] != password:
|
|
return "Неверный логин или пароль", 401
|
|
|
|
# Проверяем, что пользователь является плательщиком
|
|
payments = load_payments()
|
|
payment = None
|
|
for p in payments:
|
|
if p["id"] == payment_id:
|
|
payment = p
|
|
break
|
|
|
|
if not payment:
|
|
return "Платеж не найден", 404
|
|
|
|
if payment["payer"] != username:
|
|
return "Вы не являетесь плательщиком по этому платежу", 403
|
|
|
|
# Выполняем платеж
|
|
success, message = process_payment(payment_id, username)
|
|
|
|
if success:
|
|
return redirect(f'/pay/{payment_id}')
|
|
else:
|
|
return f"Ошибка: {message}", 400
|
|
|
|
# ---------- ОСНОВНЫЕ МАРШРУТЫ ----------
|
|
@app.route('/', methods=['GET', 'POST'])
|
|
def login():
|
|
if request.method == 'POST':
|
|
users = load_users()
|
|
username = request.form.get('username')
|
|
password = request.form.get('password')
|
|
|
|
user_data = users.get(username)
|
|
if user_data and isinstance(user_data, dict) and user_data['password'] == password:
|
|
session['user'] = username
|
|
return redirect('/dashboard')
|
|
else:
|
|
return render_login().replace('</form>',
|
|
'<div class="alert alert-danger mt-3">Неверный логин или пароль</div></form>')
|
|
|
|
return render_login()
|
|
|
|
@app.route('/register', methods=['GET', 'POST'])
|
|
def register():
|
|
if request.method == 'POST':
|
|
users = load_users()
|
|
username = request.form.get('username')
|
|
|
|
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,
|
|
'role': 'client',
|
|
'full_name': f"{request.form.get('first_name')} {request.form.get('last_name')}",
|
|
'email': request.form.get('email'),
|
|
'api_key': None
|
|
}
|
|
|
|
users[username] = user_data
|
|
save_users(users)
|
|
|
|
session['user'] = 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']]
|
|
user_cards = get_user_cards(session['user'])
|
|
shop_data = load_shop()
|
|
|
|
return render_dashboard(user_data, user_history[-20:], user_cards, shop_data)
|
|
|
|
@app.route('/generate_api_key', methods=['POST'])
|
|
@login_required
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
except Exception as e:
|
|
print(f"Transfer error: {e}")
|
|
pass
|
|
|
|
return redirect('/dashboard')
|
|
|
|
@app.route('/create_card', methods=['POST'])
|
|
@login_required
|
|
def create_card():
|
|
users = load_users()
|
|
user = users.get(session['user'])
|
|
card_type = request.form.get('card_type', 'standard')
|
|
|
|
costs = {
|
|
'standard': 0,
|
|
'gold': 50,
|
|
'platinum': 100
|
|
}
|
|
|
|
cost = costs.get(card_type, 0)
|
|
|
|
if user and user['balance'] >= cost:
|
|
user['balance'] -= cost
|
|
save_users(users)
|
|
|
|
card = create_user_card(session['user'], user, card_type)
|
|
if card:
|
|
add_history(session['user'], 'create_card', cost, f"{card_type}_{card['number'][-4:]}")
|
|
|
|
return redirect('/dashboard')
|
|
|
|
@app.route('/delete_card/<card_id>', methods=['POST'])
|
|
@login_required
|
|
def delete_card(card_id):
|
|
cards = load_cards()
|
|
user_cards = cards.get(session['user'], [])
|
|
cards[session['user']] = [c for c in user_cards if c['id'] != card_id]
|
|
save_cards(cards)
|
|
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 = 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}")
|
|
|
|
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('admin', 'add_money', amount, username)
|
|
|
|
return redirect('/admin')
|
|
|
|
@app.route('/logout')
|
|
def logout():
|
|
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/cards</h5>
|
|
<p>Получить список карт пользователя</p>
|
|
</div>
|
|
|
|
<div class="endpoint">
|
|
<h5><span class="badge bg-success">POST</span> /api/v1/cards/create</h5>
|
|
<p>Создать карту. Требуется JSON: {"card_type": "standard"}</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-success">POST</span> /api/v1/payment/create</h5>
|
|
<p>Создать платеж. Требуется JSON: {"receiver": "...", "amount": 100, "description": "..."}</p>
|
|
</div>
|
|
|
|
<div class="endpoint">
|
|
<h5><span class="badge bg-success">POST</span> /api/v1/payment/process</h5>
|
|
<p>Выполнить платеж. Требуется JSON: {"payment_id": "..."}</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>
|
|
'''
|
|
# ================= ДЕКОРАТОР УЧИТЕЛЯ =================
|
|
|
|
from functools import wraps
|
|
|
|
def teacher_required(f):
|
|
@wraps(f)
|
|
def decorated(*args, **kwargs):
|
|
if 'user' not in session:
|
|
return redirect("/")
|
|
|
|
users = load_users()
|
|
user = users.get(session['user'])
|
|
|
|
if not user:
|
|
return redirect("/")
|
|
|
|
if user.get("role") not in ["teacher", "admin"]:
|
|
return redirect("/dashboard")
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated
|
|
|
|
# ============================================================
|
|
# ========= ГРАФИК ДИНАМИКИ СРЕДНЕГО БАЛЛА ==================
|
|
# ============================================================
|
|
|
|
@app.route("/student_avg_graph/<student>")
|
|
@login_required
|
|
@teacher_required
|
|
def student_avg_graph(student):
|
|
journal = load_journal()
|
|
|
|
if student not in journal:
|
|
return "Нет данных"
|
|
|
|
subjects = journal[student]
|
|
|
|
# собираем оценки по порядку добавления
|
|
all_grades = []
|
|
for subject, grades in subjects.items():
|
|
all_grades += grades
|
|
|
|
averages = []
|
|
current = []
|
|
|
|
for g in all_grades:
|
|
current.append(g)
|
|
averages.append(sum(current) / len(current))
|
|
|
|
plt.figure()
|
|
plt.plot(range(1, len(averages)+1), averages)
|
|
|
|
plt.xlabel("Количество оценок")
|
|
plt.ylabel("Средний балл")
|
|
plt.title(f"Динамика среднего балла {student}")
|
|
|
|
img = BytesIO()
|
|
plt.savefig(img, format="png")
|
|
plt.close()
|
|
img.seek(0)
|
|
|
|
graph_url = base64.b64encode(img.getvalue()).decode()
|
|
|
|
return f"""
|
|
<h2>📈 Динамика среднего балла: {student}</h2>
|
|
<a href="/journal">Назад</a>
|
|
<br><br>
|
|
<img src="data:image/png;base64,{graph_url}">
|
|
"""
|
|
|
|
|
|
# ============================================================
|
|
# ========= ФИНАНСОВЫЙ ГРАФИК УЧЕНИКА ========================
|
|
# ============================================================
|
|
|
|
@app.route("/student_finance_graph/<student>")
|
|
@login_required
|
|
@teacher_required
|
|
def student_finance_graph(student):
|
|
journal = load_journal()
|
|
users = load_users()
|
|
|
|
if student not in journal:
|
|
return "Нет данных"
|
|
|
|
subjects = journal[student]
|
|
|
|
# считаем баланс на основе оценок
|
|
balance_history = []
|
|
balance = 0
|
|
|
|
for subject, grades in subjects.items():
|
|
for g in grades:
|
|
balance += calculate_money_for_grade(g)
|
|
balance_history.append(balance)
|
|
|
|
if not balance_history:
|
|
return "Нет оценок"
|
|
|
|
plt.figure()
|
|
plt.plot(range(1, len(balance_history)+1), balance_history)
|
|
|
|
plt.xlabel("Количество оценок")
|
|
plt.ylabel("Баланс ($)")
|
|
plt.title(f"Финансовая динамика {student}")
|
|
|
|
img = BytesIO()
|
|
plt.savefig(img, format="png")
|
|
plt.close()
|
|
img.seek(0)
|
|
|
|
graph_url = base64.b64encode(img.getvalue()).decode()
|
|
|
|
return f"""
|
|
<h2>💰 Финансовый график: {student}</h2>
|
|
<a href="/journal">Назад</a>
|
|
<br><br>
|
|
<img src="data:image/png;base64,{graph_url}">
|
|
"""
|
|
|
|
|
|
# ============================================================
|
|
# ОБНОВЛЕНИЕ ТАБЛИЦЫ В ЖУРНАЛЕ (добавление ссылок)
|
|
# ============================================================
|
|
|
|
@app.route("/journal")
|
|
@login_required
|
|
@teacher_required
|
|
def journal_page():
|
|
journal = load_journal()
|
|
|
|
table = ""
|
|
|
|
for student, subjects in journal.items():
|
|
total = []
|
|
for grades in subjects.values():
|
|
total += grades
|
|
|
|
avg = round(sum(total)/len(total),2) if total else 0
|
|
|
|
table += f"""
|
|
<tr>
|
|
<td>{student}</td>
|
|
<td>{avg}</td>
|
|
<td>
|
|
<a href="/student_graph/{student}">📊 Успеваемость</a><br>
|
|
<a href="/student_avg_graph/{student}">📈 Средний</a><br>
|
|
<a href="/student_finance_graph/{student}">💰 Финансы</a>
|
|
</td>
|
|
</tr>
|
|
"""
|
|
|
|
return f"""
|
|
<h2>Электронный журнал PRO MAX</h2>
|
|
<a href="/dashboard">Назад</a>
|
|
<hr>
|
|
|
|
<h3>Создать класс</h3>
|
|
<form method="post" action="/add_class">
|
|
<input name="class_name" placeholder="5А" required>
|
|
<button>Создать</button>
|
|
</form>
|
|
|
|
<h3>Добавить ученика в класс</h3>
|
|
<form method="post" action="/add_student_to_class">
|
|
<input name="student" placeholder="Логин ученика" required>
|
|
<input name="class_name" placeholder="Класс" required>
|
|
<button>Добавить</button>
|
|
</form>
|
|
|
|
<h3>Поставить оценку</h3>
|
|
<form method="post" action="/add_grade">
|
|
<input name="student" placeholder="Логин ученика" required>
|
|
<input name="subject" placeholder="Предмет" required>
|
|
<input name="grade" type="number" min="2" max="5" required>
|
|
<button>Поставить</button>
|
|
</form>
|
|
|
|
<hr>
|
|
<h3>Ученики</h3>
|
|
<table border="1" cellpadding="10">
|
|
<tr>
|
|
<th>Ученик</th>
|
|
<th>Средний балл</th>
|
|
<th>Графики</th>
|
|
</tr>
|
|
{table}
|
|
</table>
|
|
|
|
<br>
|
|
<a href="/rating">🏆 Смотреть рейтинг</a>
|
|
"""
|
|
|
|
|
|
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()
|
|
app.run(host='0.0.0.0', port=5000, debug=True) |