/
direrome
/
project
Обзор
Документация
Войти
/
direrome
/
project
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
app.py
629 строк
27 KB
denis direrome
x
02 июн 2025, 16:51
02 июн 2025, 16:51
bd43436
Код
Авторство
О чём код?
from flask import Flask, request, jsonify, send_from_directory, session, redirect, url_for, Response from flask_mail import Mail, Message import sqlite3 from werkzeug.security import generate_password_hash, check_password_hash import os import uuid import time import logging import subprocess from datetime import datetime, timedelta from functools import wraps # Настройка Flask app = Flask(__name__) app.secret_key = os.getenv('SECRET_KEY', str(uuid.uuid4())) app.config['MAIL_SERVER'] = 'smtp.office365.com' app.config['MAIL_PORT'] = 587 app.config['MAIL_USE_TLS'] = True app.config['MAIL_USERNAME'] = os.getenv('MAIL_USERNAME', '22200445@outlook.com') app.config['MAIL_PASSWORD'] = os.getenv('****') app.config['PERMANENT_SESSION_LIFETIME'] = 604800 # 7 days app.static_folder = 'static' os.makedirs('temp', exist_ok=True) mail = Mail(app) # Настройка логирования logging.basicConfig( level=logging.INFO, filename='app.log', format='%(asctime)s - %(levelname)s - %(message)s' ) # Ограничение попыток входа login_attempts = {} MAX_ATTEMPTS = 5 LOCKOUT_TIME = 300 # 5 минут # Инициализация базы данных def init_db(): try: with sqlite3.connect('users.db') as conn: c = conn.cursor() c.execute(''' CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, email TEXT UNIQUE NOT NULL, twofa_enabled BOOLEAN DEFAULT FALSE, twofa_secret TEXT ) ''') c.execute(''' CREATE TABLE IF NOT EXISTS analytics ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, date TEXT, marketplace TEXT, sales REAL, revenue REAL, expenses REAL, FOREIGN KEY (user_id) REFERENCES users(id) ) ''') c.execute(''' CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, name TEXT NOT NULL, price REAL NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ) ''') c.execute(''' CREATE TABLE IF NOT EXISTS orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, product_id INTEGER, product_name TEXT, status TEXT, created_at TEXT, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (product_id) REFERENCES products(id) ) ''') c.execute(''' CREATE TABLE IF NOT EXISTS integrations ( id TEXT PRIMARY KEY, user_id INTEGER, marketplace TEXT NOT NULL, api_key TEXT NOT NULL, status TEXT NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ) ''') c.execute("INSERT OR IGNORE INTO users (username, password_hash, email) VALUES (?, ?, ?)", ('testuser', generate_password_hash('testpass'), 'test@example.com')) c.execute("INSERT OR IGNORE INTO analytics (user_id, date, marketplace, sales, revenue, expenses) VALUES (?, ?, ?, ?, ?, ?)", (1, '2025-01-01', 'ozon', 500.0, 2500.0, 800.0)) c.execute("INSERT OR IGNORE INTO analytics (user_id, date, marketplace, sales, revenue, expenses) VALUES (?, ?, ?, ?, ?, ?)", (1, '2025-01-02', 'wildberries', 600.0, 3000.0, 1200.0)) c.execute("INSERT OR IGNORE INTO products (user_id, name, price) VALUES (?, ?, ?)", (1, 'Test Product', 500.0)) c.execute("INSERT OR IGNORE INTO orders (user_id, product_id, product_name, status, created_at) VALUES (?, ?, ?, ?, ?)", (1, 1, 'Test Product', 'pending', '2025-01-01 10:00:00')) c.execute("INSERT OR IGNORE INTO integrations (id, user_id, marketplace, api_key, status) VALUES (?, ?, ?, ?, ?)", (str(uuid.uuid4()), 1, 'ozon', 'mock_key_123', 'active')) conn.commit() logging.info("Database initialized successfully") except sqlite3.Error as e: logging.error(f"Database initialization failed: {e}") raise # Проверка аутентификации def is_authenticated(): return 'user_id' in session # Декоратор для проверки аутентификации def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): if not is_authenticated(): return jsonify({'message': 'Требуется авторизация'}), 401 return f(*args, **kwargs) return decorated_function # Декоратор для защиты от CSRF def csrf_protect(f): @wraps(f) def decorated_function(*args, **kwargs): if request.method in ['POST', 'PUT', 'DELETE']: token = request.headers.get('X-CSRF-Token') if not token or token != session.get('csrf_token'): logging.warning(f"Invalid CSRF token for request: {request.path}") return jsonify({'message': 'Неверный CSRF-токен'}), 403 return f(*args, **kwargs) return decorated_function # Проверка попыток входа def check_login_attempts(email): if email in login_attempts: attempts, last_time = login_attempts[email] if attempts >= MAX_ATTEMPTS and time.time() - last_time < LOCKOUT_TIME: return True return False # Моковая генерация 2FA кода def generate_2fa_code(): return '123456' # В продакшене использовать pyotp # Моковая отправка 2FA кода def send_2fa_code(email, code): logging.info(f"2FA code {code} sent to {email}") # Главная страница @app.route('/') def serve_index(): return send_from_directory('static', 'index.html') # Личный кабинет @app.route('/dashboard') def serve_dashboard(): if not is_authenticated(): return redirect(url_for('serve_index')) return send_from_directory('static', 'dashboard.html') # Статические файлы @app.route('/static/<path:path>') def serve_static(path): try: return send_from_directory(app.static_folder, path) except FileNotFoundError: logging.error(f"Static file not found: {path}") return jsonify({'message': 'Файл не найден'}), 404 # CSRF-токен @app.route('/api/csrf_token', methods=['GET']) def get_csrf_token(): if 'csrf_token' not in session: session['csrf_token'] = str(uuid.uuid4()) return jsonify({'csrf_token': session['csrf_token']}) # Регистрация @app.route('/api/register', methods=['POST']) @csrf_protect def register(): try: data = request.get_json() username = data.get('username') email = data.get('email') password = data.get('password') enable_2fa = data.get('enable2fa', False) if not username or not email or not password or len(password) < 8: logging.warning("Registration failed: Invalid data") return jsonify({'message': 'Неверные данные'}), 400 twofa_secret = generate_2fa_code() if enable_2fa else None with sqlite3.connect('users.db') as conn: c = conn.cursor() c.execute("INSERT INTO users (username, password_hash, email, twofa_enabled, twofa_secret) VALUES (?, ?, ?, ?, ?)", (username, generate_password_hash(password), email, enable_2fa, twofa_secret)) conn.commit() logging.info(f"User registered: {username}") if enable_2fa: send_2fa_code(email, twofa_secret) return jsonify({'message': 'Регистрация прошла успешно'}), 201 except sqlite3.IntegrityError: logging.warning(f"Registration failed: Username {username} or email {email} exists") return jsonify({'message': 'Имя пользователя или email уже существуют'}), 409 except Exception as e: logging.error(f"Registration error: {e}") return jsonify({'message': 'Ошибка сервера'}), 500 # Вход @app.route('/api/login', methods=['POST']) @csrf_protect def login(): try: data = request.get_json() username = data.get('username') password = data.get('password') twofa_code = data.get('twofaCode', '') email = data.get('email') # Not used for login, but can be logged if check_login_attempts(username): logging.warning(f"Login blocked for {username}: Too many attempts") return jsonify({'message': 'Слишком много попыток входа. Попробуйте позже.'}), 429 with sqlite3.connect('users.db') as conn: c = conn.cursor() c.execute("SELECT id, password_hash, twofa_enabled, twofa_secret FROM users WHERE username = ?", (username,)) user = c.fetchone() if not user or not check_password_hash(user[1], password): login_attempts[username] = login_attempts.get(username, [0, time.time()]) login_attempts[username][0] += 1 login_attempts[username][1] = time.time() logging.warning(f"Failed login attempt for {username}") return jsonify({'message': 'Неверный логин или пароль'}), 401 if user[2] and twofa_code != user[3]: send_2fa_code(email, user[3]) logging.warning(f"Invalid 2FA code for {username}") return jsonify({'message': 'Неверный код 2FA'}), 401 session['user_id'] = user[0] session.permanent = True # Make session persistent if username in login_attempts: del login_attempts[username] logging.info(f"User logged in: {username}") return jsonify({'message': 'Вход выполнен успешно'}), 200 except Exception as e: logging.error(f"Login error: {e}") return jsonify({'message': 'Ошибка сервера'}), 500 # Выход @app.route('/api/logout', methods=['POST']) @login_required @csrf_protect def logout(): try: session.pop('user_id', None) session.pop('csrf_token', None) logging.info("User logged out") return jsonify({'message': 'Выход выполнен успешно'}), 200 except Exception as e: logging.error(f"Logout error: {e}") return jsonify({'message': 'Ошибка сервера'}), 500 # Проверка авторизации @app.route('/api/check_auth', methods=['GET']) def check_auth(): try: return jsonify({'authenticated': is_authenticated(), 'user_id': session.get('user_id')}) except Exception as e: logging.error(f"Check auth error: {e}") return jsonify({'message': 'Ошибка сервера'}), 500 # Аналитика @app.route('/api/analytics', methods=['GET']) @login_required def get_analytics(): try: start_date = request.args.get('startDate') end_date = request.args.get('endDate') with sqlite3.connect('users.db') as conn: c = conn.cursor() query = "SELECT marketplace, SUM(sales) as sales, SUM(revenue) as revenue, SUM(expenses) as expenses FROM analytics WHERE user_id = ?" params = [session['user_id']] if start_date and end_date: query += " AND date BETWEEN ? AND ?" params.extend([start_date, end_date]) query += " GROUP BY marketplace" c.execute(query, params) by_marketplace = [{'marketplace': row[0], 'sales': float(row[1]), 'revenue': float(row[2]), 'expenses': float(row[3])} for row in c.fetchall()] query_total = "SELECT SUM(sales), SUM(revenue), SUM(expenses) FROM analytics WHERE user_id = ?" params_total = [session['user_id']] if start_date and end_date: query_total += " AND date BETWEEN ? AND ?" params_total.extend([start_date, end_date]) c.execute(query_total, params_total) totals = c.fetchone() return jsonify({ 'sales': float(totals[0] or 0), 'revenue': float(totals[1] or 0), 'expenses': float(totals[2] or 0), 'byMarketplace': by_marketplace }) except Exception as e: logging.error(f"Analytics error: {e}") return jsonify({'message': 'Ошибка сервера'}), 500 # Экспорт аналитики в PDF @app.route('/api/export-analytics', methods=['GET']) @login_required def export_analytics(): try: start_date = request.args.get('startDate') end_date = request.args.get('endDate') with sqlite3.connect('users.db') as conn: c = conn.cursor() query = "SELECT SUM(sales), SUM(revenue), SUM(expenses) FROM analytics WHERE user_id = ?" params = [session['user_id']] if start_date and end_date: query += " AND date BETWEEN ? AND ?" params.extend([start_date, end_date]) c.execute(query, params) totals = c.fetchone() query = "SELECT marketplace, SUM(sales), SUM(revenue), SUM(expenses) FROM analytics WHERE user_id = ?" params = [session['user_id']] if start_date and end_date: query += " AND date BETWEEN ? AND ?" params.extend([start_date, end_date]) query += " GROUP BY marketplace" c.execute(query, params) markets = c.fetchall() try: with open('static/analytics-report.tex', 'r', encoding='utf-8') as f: template = f.read() except FileNotFoundError: logging.error("LaTeX template not found") return jsonify({'message': 'Шаблон не найден'}), 404 market_rows = '\n'.join([f"{row[0]} & {row[1]:.2f} & {row[2]:.2f} & {row[3]:.2f} & {(row[2] - row[3]):.2f} \\\\" for row in markets]) marketplace_names = ','.join([row[0] for row in markets]) sales_coords = ' '.join([f"({row[0]},{row[1]})" for row in markets]) revenue_coords = ' '.join([f"({row[0]},{row[2]})" for row in markets]) expenses_coords = ' '.join([f"({row[0]},{row[3]})" for row in markets]) latex_content = template \ .replace('\\SALES_AMOUNT', f"{totals[0] or 0:.2f}") \ .replace('\\REVENUE_AMOUNT', f"{totals[1] or 0:.2f}") \ .replace('\\EXPENSES_AMOUNT', f"{totals[2] or 0:.2f}") \ .replace('\\PROFIT_AMOUNT', f"{(totals[1] or 0) - (totals[2] or 0):.2f}") \ .replace('\\MARKETPLACE_ROWS', market_rows) \ .replace('\\MARKETPLACE_NAMES', marketplace_names) \ .replace('\\SALES_COORDS', sales_coords) \ .replace('\\REVENUE_COORDS', revenue_coords) \ .replace('\\EXPENSES_COORDS', expenses_coords) \ .replace('\\STARTDATE', start_date or 'N/A') \ .replace('\\ENDDATE', end_date or 'N/A') tex_file = 'temp/report.tex' with open(tex_file, 'w', encoding='utf-8') as f: f.write(latex_content) try: result = subprocess.run( ['latexmk', '-pdf', '-interaction=nonstopmode', tex_file], check=True, cwd='temp', stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8' ) pdf_file = 'temp/report.pdf' with open(pdf_file, 'rb') as f: pdf_data = f.read() response = Response( pdf_data, mimetype='application/pdf', headers={'Content-Disposition': 'attachment;filename=analytics_report.pdf'} ) except subprocess.CalledProcessError as e: logging.error(f"LaTeX compilation failed: {e.stderr}") return jsonify({'message': 'Ошибка генерации PDF'}), 500 except FileNotFoundError: logging.error("latexmk not found") return jsonify({'message': 'LaTeX не установлен'}), 500 finally: for ext in ['.tex', '.pdf', '.aux', '.log', '.fls', '.fdb_latexmk']: try: os.remove(f'temp/report{ext}') except FileNotFoundError: pass return response except Exception as e: logging.error(f"Export analytics error: {e}") return jsonify({'message': 'Ошибка сервера'}), 500 # Управление товарами @app.route('/api/products', methods=['GET', 'POST']) @login_required @csrf_protect def manage_products(): try: if request.method == 'GET': with sqlite3.connect('users.db') as conn: c = conn.cursor() c.execute("SELECT id, name, price FROM products WHERE user_id = ?", (session['user_id'],)) products = [{'id': row[0], 'name': row[1], 'price': float(row[2])} for row in c.fetchall()] return jsonify({'products': products}) elif request.method == 'POST': data = request.get_json() name = data.get('name') price = data.get('price') if not name or not isinstance(price, (int, float)) or price <= 0: logging.warning(f"Invalid product data by user {session['user_id']}") return jsonify({'message': 'Неверные данные о товаре'}), 400 with sqlite3.connect('users.db') as conn: c = conn.cursor() c.execute("INSERT INTO products (user_id, name, price) VALUES (?, ?, ?)", (session['user_id'], name, price)) conn.commit() logging.info(f"Product added by user {session['user_id']}: {name}") return jsonify({'message': 'Товар добавлен'}), 201 except Exception as e: logging.error(f"Products error: {e}") return jsonify({'message': 'Ошибка сервера'}), 500 @app.route('/api/products/<int:product_id>', methods=['DELETE']) @login_required @csrf_protect def delete_product(product_id): try: with sqlite3.connect('users.db') as conn: c = conn.cursor() c.execute("DELETE FROM products WHERE id = ? AND user_id = ?", (product_id, session['user_id'])) if c.rowcount > 0: conn.commit() logging.info(f"Product deleted by user {session['user_id']}: {product_id}") return jsonify({'message': 'Товар удален'}), 200 logging.warning(f"Product not found for deletion: {product_id}") return jsonify({'message': 'Товар не найден'}), 404 except Exception as e: logging.error(f"Delete product error: {e}") return jsonify({'message': 'Ошибка сервера'}), 500 # Управление заказами @app.route('/api/orders', methods=['GET']) @login_required def get_orders(): try: with sqlite3.connect('users.db') as conn: c = conn.cursor() c.execute("SELECT id, product_id, product_name, status, created_at FROM orders WHERE user_id = ?", (session['user_id'],)) orders = [{'id': row[0], 'product_id': row[1], 'product_name': row[2], 'status': row[3], 'created_at': row[4]} for row in c.fetchall()] return jsonify({'orders': orders}) except Exception as e: logging.error(f"Orders error: {e}") return jsonify({'message': 'Ошибка сервера'}), 500 @app.route('/api/orders/<int:order_id>', methods=['PUT', 'DELETE']) @login_required @csrf_protect def manage_order(order_id): try: with sqlite3.connect('users.db') as conn: c = conn.cursor() if request.method == 'PUT': data = request.get_json() status = data.get('status') if status not in ['pending', 'shipped', 'delivered']: logging.warning(f"Invalid order status by user {session['user_id']}: {status}") return jsonify({'message': 'Неверный статус'}), 400 c.execute("UPDATE orders SET status = ? WHERE id = ? AND user_id = ?", (status, order_id, session['user_id'])) if c.rowcount > 0: conn.commit() logging.info(f"Order status updated by user {session['user_id']}: {order_id} to {status}") return jsonify({'message': 'Заказ обновлен'}), 200 logging.warning(f"Order not found for update: {order_id}") return jsonify({'message': 'Заказ не найден'}), 404 elif request.method == 'DELETE': c.execute("DELETE FROM orders WHERE id = ? AND user_id = ?", (order_id, session['user_id'])) if c.rowcount > 0: conn.commit() logging.info(f"Order deleted by user {session['user_id']}: {order_id}") return jsonify({'message': 'Заказ удален'}), 200 logging.warning(f"Order not found for deletion: {order_id}") return jsonify({'message': 'Заказ не найден'}), 404 except Exception as e: logging.error(f"Manage order error: {e}") return jsonify({'message': 'Ошибка сервера'}), 500 # Управление интеграциями @app.route('/api/integrations', methods=['GET', 'POST']) @login_required @csrf_protect def manage_integrations(): try: if request.method == 'GET': with sqlite3.connect('users.db') as conn: c = conn.cursor() c.execute("SELECT id, marketplace, status FROM integrations WHERE user_id = ?", (session['user_id'],)) integrations = [{'id': row[0], 'marketplace': row[1], 'status': row[2]} for row in c.fetchall()] return jsonify({'integrations': integrations}) elif request.method == 'POST': data = request.get_json() marketplace = data.get('marketplace') api_key = data.get('apiKey') if not marketplace or not api_key: logging.warning(f"Invalid integration data by user {session['user_id']}") return jsonify({'message': 'Неверные данные интеграции'}), 400 integration_id = str(uuid.uuid4()) with sqlite3.connect('users.db') as conn: c = conn.cursor() c.execute("INSERT INTO integrations (id, user_id, marketplace, api_key, status) VALUES (?, ?, ?, ?, ?)", (integration_id, session['user_id'], marketplace, api_key, 'active')) conn.commit() logging.info(f"Integration added by user {session['user_id']}: {marketplace}") return jsonify({'message': 'Интеграция добавлена'}), 201 except Exception as e: logging.error(f"Integrations error: {e}") return jsonify({'message': 'Ошибка сервера'}), 500 @app.route('/api/integrations/<integration_id>', methods=['DELETE']) @login_required @csrf_protect def delete_integration(integration_id): try: with sqlite3.connect('users.db') as conn: c = conn.cursor() c.execute("DELETE FROM integrations WHERE id = ? AND user_id = ?", (integration_id, session['user_id'])) if c.rowcount > 0: conn.commit() logging.info(f"Integration deleted by user {session['user_id']}: {integration_id}") return jsonify({'message': 'Интеграция удалена'}), 200 logging.warning(f"Integration not found for deletion: {integration_id}") return jsonify({'message': 'Интеграция не найдена'}), 404 except Exception as e: logging.error(f"Delete integration error: {e}") return jsonify({'message': 'Ошибка сервера'}), 500 # Обратная связь @app.route('/api/contact', methods=['POST']) @csrf_protect def contact(): try: data = request.get_json() name = data.get('name') email = data.get('email') message = data.get('message') if not name or not email or not message: logging.warning('Invalid contact form submission') return jsonify({'message': 'Заполните все поля'}), 400 if not app.config.get('MAIL_USERNAME'): logging.error('MAIL_USERNAME is not configured') return jsonify({'message': 'Ошибка конфигурации сервера'}), 500 # Отправка email msg = Message( subject='Новое сообщение с сайта', sender=app.config['MAIL_USERNAME'], recipients=['artem.kobzar2006@mail.ru.com'], body=f'Имя: {name}\nEmail: {email}\nСообщение: {message}' ) mail.send(msg) logging.info(f'Contact message from {name} ({email}) sent') return jsonify({'message': 'Сообщение отправлено'}), 200 except Exception as e: logging.error(f'Contact form error: {str(e)}') return jsonify({'message': f'Ошибка сервера: {str(e)}'}), 500 # Настройки (смена пароля) @app.route('/api/settings', methods=['PUT']) @login_required @csrf_protect def update_settings(): try: data = request.get_json() current_password = data.get('currentPassword') new_password = data.get('newPassword') if not current_password or not new_password or len(new_password) < 8: logging.warning(f"Invalid password update attempt by user {session['user_id']}") return jsonify({'message': 'Текущий и новый пароль (минимум 8 символов) обязательны'}), 400 with sqlite3.connect('users.db') as conn: c = conn.cursor() c.execute("SELECT password_hash FROM users WHERE id = ?", (session['user_id'],)) user = c.fetchone() if not user or not check_password_hash(user[0], current_password): logging.warning(f"Incorrect current password for user {session['user_id']}") return jsonify({'message': 'Неверный текущий пароль'}), 401 c.execute("UPDATE users SET password_hash = ? WHERE id = ?", (generate_password_hash(new_password), session['user_id'])) conn.commit() logging.info(f"Password updated for user {session['user_id']}") return jsonify({'message': 'Пароль изменен'}), 200 except Exception as e: logging.error(f"Settings error: {e}") return jsonify({'message': 'Ошибка сервера'}), 500 if __name__ == '__main__': try: init_db() app.run(debug=True, host='0.0.0.0', port=8080) except Exception as e: logging.error(f"Server startup failed: {e}") print(f"Failed to start server: {e}")