базовые настройки
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
from flask import Flask, request, jsonify
|
||||
from config import Config
|
||||
from db import MedodsAPI, db, VkPost
|
||||
from db import VkAPI, db, MedodsAPI, ApiEndpoint
|
||||
from scheduler import start_scheduler
|
||||
from token_utils import generate_token
|
||||
from http_client import send_request
|
||||
import logging
|
||||
import os
|
||||
@@ -33,16 +32,28 @@ def index():
|
||||
return render_template("index.html")
|
||||
|
||||
|
||||
@app.route("/medods")
|
||||
@app.route("/medods", methods=["GET"])
|
||||
def medods():
|
||||
medods_api = MedodsAPI.query.first()
|
||||
data = {"url": medods_api.url if medods_api else "none"}
|
||||
data = {}
|
||||
if medods_api:
|
||||
apiKey = False
|
||||
if medods_api.identity and medods_api.secretKey:
|
||||
apiKey = True
|
||||
data = {
|
||||
"url": medods_api.url,
|
||||
"apiKey": apiKey,
|
||||
}
|
||||
return render_template("medods.html", data=data)
|
||||
|
||||
|
||||
@app.route("/vk")
|
||||
def vk():
|
||||
return render_template("vk.html")
|
||||
vkDB = VkAPI.query.first()
|
||||
data = {}
|
||||
if vkDB:
|
||||
data = {"vk_settings": vkDB.toDict()}
|
||||
return render_template("vk.html", data=data)
|
||||
|
||||
|
||||
@app.route("/posts")
|
||||
@@ -57,66 +68,153 @@ def init():
|
||||
logger.info("Приложение запущено")
|
||||
|
||||
|
||||
@app.route("/settings/medods_url", methods=["POST"])
|
||||
@app.route("/settings/medods", methods=["POST"])
|
||||
def medods_url():
|
||||
data = request.json
|
||||
db.session.merge(MedodsAPI(url=data.get("url", "http://10.3.10.10/api/v2/")))
|
||||
db.session.commit()
|
||||
logger.info(data)
|
||||
return jsonify({"status": "ok"})
|
||||
apiKey = data.get("apiKey", None)
|
||||
url = data.get("url", None)
|
||||
if url is not None:
|
||||
logger.info("Получен url")
|
||||
try:
|
||||
medodsRecord = MedodsAPI.query.first()
|
||||
medodsRecord.url = url
|
||||
db.session.commit()
|
||||
logger.info("Обновлен url")
|
||||
except Exception:
|
||||
db.session.merge(MedodsAPI(url=url))
|
||||
db.session.commit()
|
||||
logger.info("Добавлен url")
|
||||
|
||||
if apiKey:
|
||||
logger.info("Получены ключи")
|
||||
try:
|
||||
medodsRecord = MedodsAPI.query.first()
|
||||
medodsRecord.identity = apiKey["identity"]
|
||||
medodsRecord.secretKey = apiKey["secretKey"]
|
||||
db.session.commit()
|
||||
logger.info("Обновлены ключи")
|
||||
except Exception:
|
||||
db.session.merge(
|
||||
MedodsAPI(identity=apiKey["identity"], secretKey=apiKey["secretKey"])
|
||||
)
|
||||
db.session.commit()
|
||||
logger.info("Добавлены ключи")
|
||||
return jsonify({"ok": True})
|
||||
# return jsonify({"ok": False}), 400
|
||||
|
||||
|
||||
@app.route("/settings/medods_apikey", methods=["POST"])
|
||||
def medods_apikey():
|
||||
data = request.json
|
||||
logger.info(data)
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
|
||||
@app.route("/settings/requests", methods=["GET", "POST", "PATCH"])
|
||||
@app.route("/settings/requests", methods=["GET", "POST", "PATCH", "DELETE"])
|
||||
def get_requests():
|
||||
requestData = (
|
||||
request.json if request.method in ["POST", "PATCH", "DELETE"] else None
|
||||
)
|
||||
match request.method:
|
||||
case "DELETE":
|
||||
logger.info("Удален запрос")
|
||||
logger.info(requestData)
|
||||
return jsonify({"status": "ok"})
|
||||
case "POST":
|
||||
logger.info("Добавлен новый запрос")
|
||||
logger.info(request.json)
|
||||
logger.info("Добавлен/обновлен запрос")
|
||||
if "id" in requestData:
|
||||
logger.info("Обновлен запрос")
|
||||
try:
|
||||
db.session.execute(
|
||||
db.update(ApiEndpoint)
|
||||
.where(ApiEndpoint.id == requestData["id"])
|
||||
.values(
|
||||
method=requestData.get("method"),
|
||||
title=requestData.get("title"),
|
||||
url_path=requestData.get("url_path"),
|
||||
payload=requestData.get("payload", {}),
|
||||
query_params=requestData.get("query_params", {}),
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обновлении запроса: {e}")
|
||||
return jsonify({"status": "ok"})
|
||||
else:
|
||||
logger.info("Добавлен запрос")
|
||||
try:
|
||||
db.session.merge(ApiEndpoint(**requestData))
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при добавлении запроса: {e}")
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
case "PATCH":
|
||||
logger.info("Обновлен запрос")
|
||||
logger.info(request.json)
|
||||
return jsonify({"status": "ok"})
|
||||
logger.info("Выполнен запрос")
|
||||
requestParams = ApiEndpoint.query.filter_by(id=requestData["id"]).first()
|
||||
medodsDB = MedodsAPI.query.first()
|
||||
baseUrl = medodsDB.url
|
||||
response = send_request(
|
||||
requestParams.method,
|
||||
f"{baseUrl}{requestParams.url_path}",
|
||||
requestParams.payload,
|
||||
requestParams.query_params,
|
||||
)
|
||||
exitData = {}
|
||||
try:
|
||||
exitData = response.json()
|
||||
except:
|
||||
exitData = response.text
|
||||
return jsonify(exitData)
|
||||
|
||||
case "GET":
|
||||
logger.info("Получен список запросов")
|
||||
|
||||
requestsDB = ApiEndpoint.query.all()
|
||||
requestsList = [r.toDict() for r in requestsDB]
|
||||
logger.info(requestsList)
|
||||
return jsonify(
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"method": "GET",
|
||||
"title": "Получить список пользователей",
|
||||
"url_path": "/users",
|
||||
"payload": {},
|
||||
"query": {"limit": 10, "offset": 0},
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"method": "POST",
|
||||
"title": "Добавить пост в очередь",
|
||||
"url_path": "/scheduler",
|
||||
"payload": {
|
||||
"text": "Текст поста",
|
||||
"image": "path/to/image.jpg",
|
||||
},
|
||||
"query": {},
|
||||
},
|
||||
]
|
||||
{
|
||||
"status": "ok",
|
||||
"requests": requestsList,
|
||||
}
|
||||
)
|
||||
|
||||
case _:
|
||||
logger.error("Неверный метод запроса")
|
||||
return jsonify({"status": "error"})
|
||||
logger.error("Неверный метод запроса")
|
||||
return jsonify({"status": "error"}), 405
|
||||
|
||||
|
||||
@app.route("/token", methods=["GET"])
|
||||
def token():
|
||||
return jsonify({"token": generate_token()})
|
||||
@app.route("/settings/vk", methods=["POST"])
|
||||
def settings_vk():
|
||||
requestData = request.json
|
||||
logger.info(requestData)
|
||||
if "id" in requestData:
|
||||
logger.info("Обновлен запрос")
|
||||
try:
|
||||
db.session.execute(
|
||||
db.update(VkAPI)
|
||||
.where(VkAPI.id == requestData["id"])
|
||||
.values(
|
||||
group_id=(
|
||||
int(requestData.get("group_id", 0))
|
||||
if requestData.get("group_id")
|
||||
else 0
|
||||
),
|
||||
access_token=requestData.get("access_token"),
|
||||
base_photo_url=(
|
||||
int(requestData.get("base_photo_url", 0))
|
||||
if requestData.get("base_photo_url")
|
||||
else 0
|
||||
),
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обновлении запроса: {e}")
|
||||
return jsonify({"status": "ok"})
|
||||
else:
|
||||
logger.info("Добавлен запрос")
|
||||
try:
|
||||
db.session.merge(VkAPI(**requestData))
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при добавлении запроса: {e}")
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
|
||||
@app.route("/request", methods=["POST"])
|
||||
@@ -128,16 +226,5 @@ def make_request():
|
||||
return jsonify({"status": response.status_code})
|
||||
|
||||
|
||||
@app.route("/vk/post", methods=["POST"])
|
||||
def add_post():
|
||||
data = request.json
|
||||
post = VkPost(
|
||||
text=data["text"], image_path=data.get("image"), publish_at=data["publish_at"]
|
||||
)
|
||||
db.session.add(post)
|
||||
db.session.commit()
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True)
|
||||
|
||||
@@ -4,28 +4,64 @@ from datetime import datetime
|
||||
db = SQLAlchemy()
|
||||
|
||||
|
||||
class HttpRequestLog(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
method = db.Column(db.String(10))
|
||||
url = db.Column(db.Text)
|
||||
request_data = db.Column(db.Text)
|
||||
response_code = db.Column(db.Integer)
|
||||
response_body = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
|
||||
|
||||
class VkPost(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
text = db.Column(db.Text)
|
||||
image_path = db.Column(db.Text)
|
||||
publish_at = db.Column(db.DateTime)
|
||||
published = db.Column(db.Boolean, default=False)
|
||||
|
||||
|
||||
class MedodsAPI(db.Model):
|
||||
__tablename__ = "medods_api"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
url = db.Column(db.Text)
|
||||
identity = db.Column(db.Text)
|
||||
secretKey = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
|
||||
class ApiEndpoint(db.Model):
|
||||
__tablename__ = "api_endpoints"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
method = db.Column(db.String(10), nullable=False) # GET / POST
|
||||
title = db.Column(db.String(255), nullable=False) # Человеческое описание
|
||||
url_path = db.Column(db.String(255), nullable=False) # /users, /scheduler
|
||||
|
||||
payload = db.Column(db.JSON, default=dict) # Тело запроса
|
||||
query_params = db.Column(db.JSON, default=dict) # Query-параметры
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ApiEndpoint {self.method} {self.url_path}>"
|
||||
|
||||
def toDict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"method": self.method,
|
||||
"title": self.title,
|
||||
"url_path": self.url_path,
|
||||
"payload": self.payload,
|
||||
"query_params": self.query_params,
|
||||
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"updated_at": self.updated_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
}
|
||||
|
||||
|
||||
class VkAPI(db.Model):
|
||||
__tablename__ = "vk_settings"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
group_id = db.Column(db.Integer)
|
||||
access_token = db.Column(db.Text)
|
||||
base_photo_url = db.Column(db.Integer)
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
def toDict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"group_id": self.group_id,
|
||||
"access_token": self.access_token,
|
||||
"base_photo_url": self.base_photo_url,
|
||||
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"updated_at": self.updated_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
}
|
||||
|
||||
+20
-13
@@ -1,20 +1,27 @@
|
||||
import requests
|
||||
from db import db, HttpRequestLog
|
||||
import json
|
||||
|
||||
from token_utils import generate_token
|
||||
|
||||
|
||||
def send_request(method, url, data=None, headers=None):
|
||||
response = requests.request(method=method, url=url, json=data, headers=headers)
|
||||
def send_request(method, url, json_data=None, params=None):
|
||||
from app import logger
|
||||
|
||||
log = HttpRequestLog(
|
||||
method=method,
|
||||
url=url,
|
||||
request_data=json.dumps(data, ensure_ascii=False),
|
||||
response_code=response.status_code,
|
||||
response_body=response.text,
|
||||
)
|
||||
headers = {"Content-Type": "application/json"}
|
||||
bearer_token = generate_token()
|
||||
if bearer_token:
|
||||
headers["Authorization"] = f"Bearer {bearer_token}"
|
||||
logger.info(headers)
|
||||
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
try:
|
||||
response = requests.request(
|
||||
method=method, url=url, params=params, json=json_data, headers=headers
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при выполнении запроса: {e}")
|
||||
return
|
||||
|
||||
logger.info(response.status_code)
|
||||
logger.info(response.headers)
|
||||
# logger.info(response.text)
|
||||
|
||||
return response
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
padding-top: 64px; /* чтобы fixed navbar не перекрывал контент */
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.navbar .nav-link.active {
|
||||
color: #fff !important;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/* Стили для разделов */
|
||||
.url-status {
|
||||
border-left: 4px solid #0dcaf0;
|
||||
}
|
||||
|
||||
.api-status {
|
||||
border-left: 4px solid #198754;
|
||||
}
|
||||
|
||||
/* Стили для списка запросов */
|
||||
.requests-list {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.request-item {
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
border-left: 3px solid transparent;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.request-item:hover {
|
||||
background-color: rgba(13, 110, 253, 0.05);
|
||||
}
|
||||
|
||||
.request-item.active {
|
||||
background-color: rgba(13, 110, 253, 0.1);
|
||||
border-left-color: #0d6efd;
|
||||
}
|
||||
|
||||
/* Метод запроса */
|
||||
.method-badge {
|
||||
font-size: 0.75em;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Стили для JSON */
|
||||
.json-key { color: #005cc5; font-weight: 600; }
|
||||
.json-string { color: #032f62; }
|
||||
.json-number { color: #e36209; }
|
||||
.json-boolean { color: #6f42c1; }
|
||||
.json-null { color: #d73a49; }
|
||||
|
||||
/* Параметры */
|
||||
.param-row {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.param-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Кнопки действий */
|
||||
.btn-action {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Респонс контейнер */
|
||||
.response-container {
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', Consolas, 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.response-pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Статус индикаторы */
|
||||
.status-indicator {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Анимации */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/* Стили статуса */
|
||||
.status-badge {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Карточки настроек */
|
||||
.setting-card {
|
||||
border-left: 4px solid #007bff;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.setting-card:hover {
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 123, 255, 0.15);
|
||||
}
|
||||
|
||||
.setting-card .card-header {
|
||||
background-color: rgba(0, 123, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Иконки VK */
|
||||
.vk-icon {
|
||||
color: #0077FF;
|
||||
/* Официальный цвет VK */
|
||||
}
|
||||
|
||||
/* Анимации */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Уведомления */
|
||||
.alert-fixed {
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: 20px;
|
||||
z-index: 1050;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
/* Chrome, Edge, Safari */
|
||||
input[type=number]::-webkit-outer-spin-button,
|
||||
input[type=number]::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
@@ -0,0 +1,668 @@
|
||||
// Глобальные переменные
|
||||
let currentRequestId = null;
|
||||
let requestsData = [];
|
||||
|
||||
// Инициализация при загрузке страницы
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
loadRequests();
|
||||
updateStatusIndicators();
|
||||
addQueryParam();
|
||||
addPayloadParam();
|
||||
});
|
||||
|
||||
// Обновление индикаторов статуса
|
||||
function updateStatusIndicators() {
|
||||
const urlCheck = document.getElementById('urlCheck');
|
||||
const apiKeyCheck = document.getElementById('apiKeyCheck');
|
||||
const saveServerUrlButton = document.getElementById('saveServerUrlButton');
|
||||
const uploadApiKeyButton = document.getElementById('uploadApiKeyButton');
|
||||
|
||||
// URL индикатор
|
||||
if (pageData && pageData.url) {
|
||||
urlCheck.innerHTML = '<span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>Настроен</span>';
|
||||
saveServerUrlButton.innerHTML = '<i class="bi bi-arrow-repeat me-2"></i>Обновить URL';
|
||||
saveServerUrlButton.classList.remove('btn-success');
|
||||
saveServerUrlButton.classList.add('btn-outline-success');
|
||||
} else {
|
||||
urlCheck.innerHTML = '<span class="badge bg-warning"><i class="bi bi-exclamation-triangle me-1"></i>Не настроен</span>';
|
||||
saveServerUrlButton.innerHTML = '<i class="bi bi-save me-2"></i>Сохранить URL';
|
||||
saveServerUrlButton.classList.remove('btn-outline-success');
|
||||
saveServerUrlButton.classList.add('btn-success');
|
||||
}
|
||||
|
||||
// API Key индикатор
|
||||
if (pageData && pageData.apiKey) {
|
||||
apiKeyCheck.innerHTML = '<span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>Загружен</span>';
|
||||
uploadApiKeyButton.innerHTML = '<i class="bi bi-arrow-repeat me-2"></i>Обновить ключ';
|
||||
uploadApiKeyButton.classList.remove('btn-primary');
|
||||
uploadApiKeyButton.classList.add('btn-outline-primary');
|
||||
} else {
|
||||
apiKeyCheck.innerHTML = '<span class="badge bg-warning"><i class="bi bi-exclamation-triangle me-1"></i>Не загружен</span>';
|
||||
uploadApiKeyButton.innerHTML = '<i class="bi bi-upload me-2"></i>Загрузить API ключ';
|
||||
uploadApiKeyButton.classList.remove('btn-outline-primary');
|
||||
uploadApiKeyButton.classList.add('btn-primary');
|
||||
}
|
||||
}
|
||||
|
||||
// Сохранение URL сервера
|
||||
async function saveServerUrl() {
|
||||
const serverUrl = document.getElementById('server_url').value.trim();
|
||||
|
||||
if (!serverUrl) {
|
||||
showAlert('warning', 'Введите URL сервера');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/settings/medods', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: serverUrl })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.pageData = window.pageData || {};
|
||||
window.pageData.url = serverUrl;
|
||||
showAlert('success', 'URL сервера сохранен!');
|
||||
updateStatusIndicators();
|
||||
} else {
|
||||
const error = await response.text();
|
||||
showAlert('danger', 'Ошибка: ' + error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
showAlert('danger', 'Ошибка сохранения!');
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка API ключа
|
||||
async function uploadApiKey() {
|
||||
const fileInput = document.getElementById('api_key_file');
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (!file) {
|
||||
showAlert('warning', 'Выберите CSV файл');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
const lines = text.trim().split('\n');
|
||||
|
||||
if (lines.length < 2) {
|
||||
showAlert('warning', 'Файл должен содержать минимум 2 строки');
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = lines[0].split(';').map(h => h.trim());
|
||||
if (!headers.includes('identity') || !headers.includes('secretKey')) {
|
||||
showAlert('warning', 'Файл должен содержать колонки: identity и secretKey');
|
||||
return;
|
||||
}
|
||||
|
||||
const keyInfo = lines[1].split(';').map(h => h.trim());
|
||||
const apiKey = {};
|
||||
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
apiKey[headers[i]] = keyInfo[i];
|
||||
}
|
||||
|
||||
const response = await fetch('/settings/medods', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ apiKey })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.pageData = window.pageData || {};
|
||||
window.pageData.apiKey = apiKey;
|
||||
showAlert('success', 'API ключ загружен!');
|
||||
fileInput.value = '';
|
||||
updateStatusIndicators();
|
||||
} else {
|
||||
const error = await response.text();
|
||||
showAlert('danger', 'Ошибка: ' + error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
showAlert('danger', 'Ошибка загрузки файла!');
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка списка запросов
|
||||
async function loadRequests() {
|
||||
try {
|
||||
const response = await fetch('/settings/requests');
|
||||
const data = await response.json();
|
||||
requestsData = data.requests ? data.requests : [];
|
||||
renderRequestsList();
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки запросов:', error);
|
||||
showAlert('danger', 'Ошибка загрузки запросов');
|
||||
}
|
||||
}
|
||||
|
||||
// Отображение списка запросов
|
||||
function renderRequestsList() {
|
||||
const container = document.getElementById('requestsList');
|
||||
|
||||
if (requestsData.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="alert alert-info text-center">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
Нет сохраненных запросов<br>
|
||||
<small>Нажмите "Новый запрос" для создания</small>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="list-group list-group-flush">';
|
||||
|
||||
requestsData.forEach(request => {
|
||||
const methodClass = getMethodClass(request.method);
|
||||
const isActive = currentRequestId === request.id;
|
||||
|
||||
html += `
|
||||
<div class="list-group-item request-item ${isActive ? 'active' : ''}"
|
||||
onclick="editRequest(${request.id})">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="flex-grow-1 me-3">
|
||||
<div class="d-flex align-items-center mb-1">
|
||||
<span class="badge ${methodClass} method-badge me-2">${request.method}</span>
|
||||
<strong class="text-dark">${request.title}</strong>
|
||||
</div>
|
||||
<small class="text-muted d-block">${request.url_path}</small>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-success btn-action"
|
||||
onclick="event.stopPropagation(); executeRequest(${request.id})"
|
||||
title="Запустить запрос">
|
||||
<i class="bi bi-play"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-action"
|
||||
onclick="event.stopPropagation(); deleteRequest(${request.id})"
|
||||
title="Удалить запрос">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Получение класса для метода
|
||||
function getMethodClass(method) {
|
||||
const classes = {
|
||||
'GET': 'bg-primary',
|
||||
'POST': 'bg-success',
|
||||
'PUT': 'bg-warning text-dark',
|
||||
'DELETE': 'bg-danger',
|
||||
'PATCH': 'bg-info'
|
||||
};
|
||||
return classes[method] || 'bg-secondary';
|
||||
}
|
||||
|
||||
// Создание нового запроса
|
||||
function newRequest() {
|
||||
resetForm();
|
||||
document.getElementById('editorTitle').textContent = 'Создание нового запроса';
|
||||
document.getElementById('saveRequestButton').innerHTML = '<i class="bi bi-save me-1"></i>Сохранить запрос';
|
||||
document.getElementById('executeButton').disabled = true;
|
||||
currentRequestId = null;
|
||||
updateActiveItem();
|
||||
|
||||
// Скролл к редактору
|
||||
document.querySelector('.col-md-8').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
// Редактирование запроса
|
||||
function editRequest(id) {
|
||||
const request = requestsData.find(r => r.id === id);
|
||||
if (!request) return;
|
||||
|
||||
currentRequestId = id;
|
||||
document.getElementById('editorTitle').textContent = `Редактирование: ${request.title}`;
|
||||
document.getElementById('saveRequestButton').innerHTML = '<i class="bi bi-save me-1"></i>Обновить запрос';
|
||||
document.getElementById('executeButton').disabled = false;
|
||||
|
||||
// Заполняем поля формы
|
||||
document.getElementById('requestId').value = request.id;
|
||||
document.getElementById('title').value = request.title;
|
||||
document.getElementById('method').value = request.method;
|
||||
document.getElementById('url_path').value = request.url_path;
|
||||
|
||||
// Очищаем и заполняем query параметры
|
||||
document.getElementById('queryParamsContainer').innerHTML = '';
|
||||
if (request.query_params && typeof request.query_params === 'object' && Object.keys(request.query_params).length > 0) {
|
||||
Object.entries(request.query_params).forEach(([key, value]) => {
|
||||
addQueryParam(key, value);
|
||||
});
|
||||
} else {
|
||||
addQueryParam();
|
||||
}
|
||||
|
||||
// Очищаем и заполняем payload параметры
|
||||
document.getElementById('payloadParamsContainer').innerHTML = '';
|
||||
if (request.payload && typeof request.payload === 'object' && Object.keys(request.payload).length > 0) {
|
||||
Object.entries(request.payload).forEach(([key, value]) => {
|
||||
addPayloadParam(key, typeof value === 'object' ? JSON.stringify(value) : value);
|
||||
});
|
||||
} else {
|
||||
addPayloadParam();
|
||||
}
|
||||
|
||||
// Даты создания и обновления
|
||||
document.getElementById('timestampDiv').classList.remove('d-none');
|
||||
document.getElementById('createdAt').textContent = request.created_at;
|
||||
document.getElementById('updatedAt').textContent = request.updated_at;
|
||||
|
||||
updateActiveItem();
|
||||
|
||||
// Скролл к редактору
|
||||
document.querySelector('.col-md-8').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
// Обновление активного элемента в списке
|
||||
function updateActiveItem() {
|
||||
document.querySelectorAll('.request-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
|
||||
if (currentRequestId) {
|
||||
const activeItem = document.querySelector(`[onclick="editRequest(${currentRequestId})"]`);
|
||||
if (activeItem) {
|
||||
activeItem.classList.add('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Добавление параметра Query
|
||||
function addQueryParam(key = '', value = '') {
|
||||
const container = document.getElementById('queryParamsContainer');
|
||||
const paramId = Date.now() + Math.random();
|
||||
|
||||
const html = `
|
||||
<div class="param-row" id="param-${paramId}">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control param-key"
|
||||
placeholder="Ключ параметра" value="${key}">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control param-value"
|
||||
placeholder="Значение" value="${value}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger w-100"
|
||||
onclick="document.getElementById('param-${paramId}').remove()">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
// Добавление параметра Payload
|
||||
function addPayloadParam(key = '', value = '') {
|
||||
const container = document.getElementById('payloadParamsContainer');
|
||||
const paramId = Date.now() + Math.random();
|
||||
|
||||
const html = `
|
||||
<div class="param-row" id="payload-${paramId}">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control param-key"
|
||||
placeholder="Ключ параметра" value="${key}">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control param-value"
|
||||
placeholder="Значение" value="${value}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger w-100"
|
||||
onclick="document.getElementById('payload-${paramId}').remove()">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
// Сброс формы
|
||||
function resetForm() {
|
||||
document.getElementById('requestForm').reset();
|
||||
document.getElementById('requestId').value = '';
|
||||
document.getElementById('queryParamsContainer').innerHTML = '';
|
||||
document.getElementById('payloadParamsContainer').innerHTML = '';
|
||||
addQueryParam();
|
||||
addPayloadParam();
|
||||
currentRequestId = null;
|
||||
document.getElementById('editorTitle').textContent = 'Создание нового запроса';
|
||||
document.getElementById('saveRequestButton').innerHTML = '<i class="bi bi-save me-1"></i>Сохранить запрос';
|
||||
document.getElementById('executeButton').disabled = true;
|
||||
updateActiveItem();
|
||||
}
|
||||
|
||||
// Сохранение запроса
|
||||
async function saveRequest() {
|
||||
const id = document.getElementById('requestId').value;
|
||||
const title = document.getElementById('title').value.trim();
|
||||
const method = document.getElementById('method').value;
|
||||
const url_path = document.getElementById('url_path').value.trim();
|
||||
|
||||
if (!title || !method || !url_path) {
|
||||
showAlert('warning', 'Заполните все обязательные поля');
|
||||
return;
|
||||
}
|
||||
|
||||
// Собираем query параметры
|
||||
const query_params = {};
|
||||
document.querySelectorAll('#queryParamsContainer .param-row').forEach(row => {
|
||||
const key = row.querySelector('.param-key').value.trim();
|
||||
const value = row.querySelector('.param-value').value.trim();
|
||||
if (key) query_params[key] = value;
|
||||
});
|
||||
|
||||
// Собираем payload параметры
|
||||
const payload = {};
|
||||
document.querySelectorAll('#payloadParamsContainer .param-row').forEach(row => {
|
||||
const key = row.querySelector('.param-key').value.trim();
|
||||
const value = row.querySelector('.param-value').value.trim();
|
||||
if (key) {
|
||||
try {
|
||||
payload[key] = JSON.parse(value);
|
||||
} catch {
|
||||
payload[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const requestData = { title, method, url_path, query_params, payload };
|
||||
if (id) requestData.id = parseInt(id);
|
||||
|
||||
try {
|
||||
const response = await fetch('/settings/requests', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('success', 'Запрос успешно сохранен!');
|
||||
await loadRequests();
|
||||
if (id) editRequest(parseInt(id));
|
||||
} else {
|
||||
const error = await response.text();
|
||||
showAlert('danger', 'Ошибка сохранения: ' + error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
showAlert('danger', 'Ошибка сохранения запроса!');
|
||||
}
|
||||
}
|
||||
|
||||
// Удаление запроса
|
||||
async function deleteRequest(id) {
|
||||
if (!confirm('Вы уверены, что хотите удалить этот запрос?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/settings/requests/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('success', 'Запрос удален!');
|
||||
if (currentRequestId === id) resetForm();
|
||||
await loadRequests();
|
||||
} else {
|
||||
const error = await response.text();
|
||||
showAlert('danger', 'Ошибка удаления: ' + error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
showAlert('danger', 'Ошибка удаления запроса!');
|
||||
}
|
||||
}
|
||||
|
||||
// Выполнение текущего запроса
|
||||
async function executeCurrentRequest() {
|
||||
if (!currentRequestId) {
|
||||
showAlert('warning', 'Сначала выберите или создайте запрос');
|
||||
return;
|
||||
}
|
||||
await executeRequest(currentRequestId);
|
||||
}
|
||||
|
||||
// Выполнение запроса по ID
|
||||
async function executeRequest(id) {
|
||||
try {
|
||||
showLoader(true);
|
||||
const response = await fetch('/settings/requests', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: parseInt(id) })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
displayResponse(data);
|
||||
showResponseSection();
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
displayResponse({ error: error.message });
|
||||
showResponseSection();
|
||||
} finally {
|
||||
showLoader(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Отображение ответа
|
||||
function displayResponse(data) {
|
||||
const container = document.getElementById('responseContainer');
|
||||
container.innerHTML = '';
|
||||
|
||||
// Создаем пре для лучшего отображения
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'response-pre';
|
||||
pre.appendChild(formatJson(data, 0));
|
||||
container.appendChild(pre);
|
||||
|
||||
window.lastResponse = data;
|
||||
}
|
||||
|
||||
// Форматирование JSON
|
||||
function formatJson(data, indent) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
function format(value, depth) {
|
||||
const indentStr = ' '.repeat(depth);
|
||||
|
||||
if (value === null) {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'json-null';
|
||||
span.textContent = 'null';
|
||||
return span;
|
||||
} else if (typeof value === 'boolean') {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'json-boolean';
|
||||
span.textContent = value.toString();
|
||||
return span;
|
||||
} else if (typeof value === 'number') {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'json-number';
|
||||
span.textContent = value;
|
||||
return span;
|
||||
} else if (typeof value === 'string') {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'json-string';
|
||||
span.textContent = JSON.stringify(value);
|
||||
return span;
|
||||
} else if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
return document.createTextNode('[]');
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode('['));
|
||||
|
||||
value.forEach((item, index) => {
|
||||
const itemDiv = document.createElement('div');
|
||||
itemDiv.style.marginLeft = '20px';
|
||||
itemDiv.appendChild(format(item, depth + 1));
|
||||
if (index < value.length - 1) {
|
||||
itemDiv.appendChild(document.createTextNode(','));
|
||||
}
|
||||
div.appendChild(itemDiv);
|
||||
});
|
||||
|
||||
div.appendChild(document.createTextNode(']'));
|
||||
return div;
|
||||
} else if (typeof value === 'object') {
|
||||
const entries = Object.entries(value);
|
||||
if (entries.length === 0) {
|
||||
return document.createTextNode('{}');
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode('{'));
|
||||
|
||||
entries.forEach(([key, val], index) => {
|
||||
const itemDiv = document.createElement('div');
|
||||
itemDiv.style.marginLeft = '20px';
|
||||
|
||||
const keySpan = document.createElement('span');
|
||||
keySpan.className = 'json-key';
|
||||
keySpan.textContent = JSON.stringify(key) + ': ';
|
||||
itemDiv.appendChild(keySpan);
|
||||
|
||||
itemDiv.appendChild(format(val, depth + 1));
|
||||
|
||||
if (index < entries.length - 1) {
|
||||
itemDiv.appendChild(document.createTextNode(','));
|
||||
}
|
||||
|
||||
div.appendChild(itemDiv);
|
||||
});
|
||||
|
||||
div.appendChild(document.createTextNode('}'));
|
||||
return div;
|
||||
}
|
||||
|
||||
return document.createTextNode(String(value));
|
||||
}
|
||||
|
||||
fragment.appendChild(format(data, 0));
|
||||
return fragment;
|
||||
}
|
||||
|
||||
// Показать раздел с ответом
|
||||
function showResponseSection() {
|
||||
const responseCard = document.getElementById('responseCard');
|
||||
responseCard.style.display = 'block';
|
||||
|
||||
// Анимация появления
|
||||
responseCard.classList.add('fade-in');
|
||||
|
||||
// Скролл к результату
|
||||
setTimeout(() => {
|
||||
responseCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Переключение видимости ответа
|
||||
function toggleResponse() {
|
||||
const responseBody = document.getElementById('responseBody');
|
||||
const toggleBtn = document.querySelector('#responseCard .bi-chevron-up');
|
||||
|
||||
if (responseBody.style.display === 'none') {
|
||||
responseBody.style.display = 'block';
|
||||
toggleBtn.classList.remove('bi-chevron-down');
|
||||
toggleBtn.classList.add('bi-chevron-up');
|
||||
} else {
|
||||
responseBody.style.display = 'none';
|
||||
toggleBtn.classList.remove('bi-chevron-up');
|
||||
toggleBtn.classList.add('bi-chevron-down');
|
||||
}
|
||||
}
|
||||
|
||||
// Скачивание ответа
|
||||
function downloadResponse() {
|
||||
if (!window.lastResponse) {
|
||||
showAlert('warning', 'Нет данных для скачивания');
|
||||
return;
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const filename = `medods_response_${timestamp}.json`;
|
||||
const jsonStr = JSON.stringify(window.lastResponse, null, 2);
|
||||
|
||||
const blob = new Blob([jsonStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Вспомогательные функции
|
||||
function showAlert(type, message) {
|
||||
const alertContainer = document.getElementById('alertContainer');
|
||||
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type} alert-dismissible fade show shadow`;
|
||||
alert.style.minWidth = '300px';
|
||||
alert.style.maxWidth = '400px';
|
||||
|
||||
alert.innerHTML = `
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi ${getAlertIcon(type)} me-2"></i>
|
||||
<div class="flex-grow-1">${message}</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
alertContainer.appendChild(alert);
|
||||
|
||||
// Автоматическое удаление через 4 секунды
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.classList.remove('show');
|
||||
setTimeout(() => alert.remove(), 150);
|
||||
}
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
function getAlertIcon(type) {
|
||||
const icons = {
|
||||
'success': 'bi-check-circle-fill',
|
||||
'warning': 'bi-exclamation-triangle-fill',
|
||||
'danger': 'bi-x-circle-fill',
|
||||
'info': 'bi-info-circle-fill'
|
||||
};
|
||||
return icons[type] || 'bi-info-circle-fill';
|
||||
}
|
||||
|
||||
function showLoader(show) {
|
||||
const executeButton = document.getElementById('executeButton');
|
||||
if (show) {
|
||||
executeButton.innerHTML = '<i class="bi bi-hourglass-split me-1"></i>Выполняется...';
|
||||
executeButton.disabled = true;
|
||||
} else {
|
||||
executeButton.innerHTML = '<i class="bi bi-play-fill me-1"></i>Запустить';
|
||||
executeButton.disabled = !currentRequestId;
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
// Инициализация при загрузке страницы
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
updateStatusIndicators();
|
||||
});
|
||||
|
||||
// Обновление индикаторов статуса
|
||||
function updateStatusIndicators() {
|
||||
const vkStatus = document.getElementById('vkStatus');
|
||||
const saveButton = document.getElementById('saveButton');
|
||||
|
||||
if (pageData && pageData.vk_settings) {
|
||||
vkStatus.innerHTML = '<span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>Настроено</span>';
|
||||
saveButton.innerHTML = '<i class="bi bi-arrow-repeat me-1"></i>Обновить настройки';
|
||||
saveButton.classList.remove('btn-primary');
|
||||
saveButton.classList.add('btn-outline-primary');
|
||||
} else {
|
||||
vkStatus.innerHTML = '<span class="badge bg-warning"><i class="bi bi-exclamation-triangle me-1"></i>Не настроено</span>';
|
||||
saveButton.innerHTML = '<i class="bi bi-save me-1"></i>Сохранить настройки';
|
||||
saveButton.classList.remove('btn-outline-primary');
|
||||
saveButton.classList.add('btn-primary');
|
||||
}
|
||||
}
|
||||
|
||||
// Переключение видимости пароля
|
||||
function togglePassword(inputId) {
|
||||
const input = document.getElementById(inputId);
|
||||
const button = input.nextElementSibling.querySelector('i');
|
||||
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
button.classList.remove('bi-eye');
|
||||
button.classList.add('bi-eye-slash');
|
||||
} else {
|
||||
input.type = 'password';
|
||||
button.classList.remove('bi-eye-slash');
|
||||
button.classList.add('bi-eye');
|
||||
}
|
||||
}
|
||||
|
||||
// Сброс формы
|
||||
function resetForm() {
|
||||
document.getElementById('vkForm').reset();
|
||||
updateStatusIndicators();
|
||||
}
|
||||
|
||||
// Сохранение настроек VK
|
||||
async function saveVkSettings() {
|
||||
const access_token = document.getElementById('access_token').value.trim();
|
||||
const group_id = document.getElementById('group_id').value.trim();
|
||||
const base_photo_url = document.getElementById('base_photo_url').value.trim();
|
||||
|
||||
// Проверка обязательных полей
|
||||
if (!access_token || !group_id) {
|
||||
showAlert('warning', 'Заполните обязательные поля: Access Token и ID сообщества');
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверка формата group_id (должно быть число)
|
||||
if (!/^\d+$/.test(group_id)) {
|
||||
showAlert('warning', 'ID сообщества должен содержать только цифры');
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = {
|
||||
access_token,
|
||||
group_id,
|
||||
base_photo_url: base_photo_url || null
|
||||
};
|
||||
|
||||
if (pageData && pageData.vk_settings) {
|
||||
settings.id = pageData.vk_settings.id;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/settings/vk', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(settings)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
showAlert('success', 'Настройки VK успешно сохранены!');
|
||||
|
||||
updateStatusIndicators();
|
||||
|
||||
// Перезагружаем страницу для отображения обновленных данных
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
|
||||
} else {
|
||||
const error = await response.text();
|
||||
showAlert('danger', 'Ошибка сохранения: ' + error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
showAlert('danger', 'Ошибка сохранения настроек!');
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательные функции для уведомлений
|
||||
function showAlert(type, message) {
|
||||
const alertContainer = document.getElementById('alertContainer');
|
||||
|
||||
// Создаем алерт
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type} alert-dismissible fade show shadow`;
|
||||
|
||||
// Иконка для типа алерта
|
||||
const icon = getAlertIcon(type);
|
||||
|
||||
alert.innerHTML = `
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi ${icon} me-2 fs-5"></i>
|
||||
<div class="flex-grow-1">${message}</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
alertContainer.appendChild(alert);
|
||||
|
||||
// Автоматическое удаление через 5 секунд
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.classList.remove('show');
|
||||
setTimeout(() => alert.remove(), 150);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function getAlertIcon(type) {
|
||||
const icons = {
|
||||
'success': 'bi-check-circle-fill',
|
||||
'warning': 'bi-exclamation-triangle-fill',
|
||||
'danger': 'bi-x-circle-fill',
|
||||
'info': 'bi-info-circle-fill'
|
||||
};
|
||||
return icons[type] || 'bi-info-circle-fill';
|
||||
}
|
||||
|
||||
// Дополнительная проверка при вводе данных
|
||||
document.getElementById('group_id').addEventListener('input', function (e) {
|
||||
this.value = this.value.replace(/[^\d]/g, '');
|
||||
});
|
||||
|
||||
// Подсказка для base_photo_url при фокусе
|
||||
document.getElementById('base_photo_url').addEventListener('focus', function () {
|
||||
if (!this.value) {
|
||||
showAlert('info', 'Формат ID фото: photo_id (например: 7236456789)');
|
||||
}
|
||||
});
|
||||
+49
-33
@@ -3,55 +3,71 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{% block title %}Система{% endblock %}</title>
|
||||
<title>{% block title %}Control Panel{% endblock %}</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
padding-top: 60px;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f8f9fa;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="/static/css/base.css" rel="stylesheet">
|
||||
{% block styles %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top shadow-sm">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">⚙️ Control Panel</a>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-light dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
Навигация
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="/">Главная</a></li>
|
||||
<li><a class="dropdown-item" href="/medods">Medods</a></li>
|
||||
<li><a class="dropdown-item" href="/vk">VK</a></li>
|
||||
<li><a class="dropdown-item" href="/posts">Посты</a></li>
|
||||
<!-- Логотип -->
|
||||
<a class="navbar-brand fw-semibold" href="/">
|
||||
<i class="bi bi-gear-fill me-1"></i> Control Panel
|
||||
</a>
|
||||
|
||||
<!-- Кнопка для мобилок -->
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNavbar">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<!-- Меню -->
|
||||
<div class="collapse navbar-collapse" id="mainNavbar">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.path == '/' %}active{% endif %}" href="/">
|
||||
<i class="bi bi-speedometer2 me-1"></i> Главная
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.path.startswith('/medods') %}active{% endif %}" href="/medods">
|
||||
<i class="bi bi-diagram-3 me-1"></i> Medods
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.path.startswith('/vk') %}active{% endif %}" href="/vk">
|
||||
<i class="bi bi-chat-dots me-1"></i> VK
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.path.startswith('/posts') %}active{% endif %}" href="/posts">
|
||||
<i class="bi bi-file-earmark-text me-1"></i> Посты
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
const pageData = {{ data | tojson }};
|
||||
</script>
|
||||
|
||||
<div class="container-fluid">
|
||||
<main class="container-fluid pt-2">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+232
-678
@@ -1,138 +1,113 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Medods{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="/static/css/medods.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<h3 class="mb-4">🔌 Medods API</h3>
|
||||
<!-- Заголовок -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="mb-1"><i class="bi bi-plug me-2"></i>Medods API</h2>
|
||||
<p class="text-muted mb-0">Управление подключением и запросами к Medods API</p>
|
||||
</div>
|
||||
<div class="badge bg-primary fs-6 px-3 py-2">
|
||||
<i class="bi bi-activity me-1"></i>API Control
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Настройка подключения -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">Подключение к серверу</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="serverForm">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-md-9">
|
||||
<label class="form-label">URL адрес сервера</label>
|
||||
<!-- Настройка подключения -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-6 mb-4 mb-lg-0">
|
||||
<div class="card h-100 url-status">
|
||||
<div
|
||||
class="card-header bg-primary bg-opacity-10 border-primary d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="bi bi-server me-2"></i>Настройка сервера</h5>
|
||||
<span id="urlCheck" class="status-indicator">
|
||||
{% if data.url %}
|
||||
<span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>Настроен</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning"><i class="bi bi-exclamation-triangle me-1"></i>Не настроен</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">URL адрес сервера</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light"><i class="bi bi-link-45deg"></i></span>
|
||||
<input type="text" class="form-control" id="server_url" placeholder="https://api.example.com"
|
||||
required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button type="button" class="btn btn-success w-100" onclick="saveServerUrl()">
|
||||
💾 Сохранить
|
||||
</button>
|
||||
value="{{ data.url or '' }}">
|
||||
</div>
|
||||
<div class="form-text small">Введите полный URL вашего сервера Medods API</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<form id="apiKeyForm" enctype="multipart/form-data">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-md-9">
|
||||
<label class="form-label">Загрузка API ключа</label>
|
||||
<input type="file" class="form-control" id="api_key_file" accept=".csv" required>
|
||||
<div class="form-text">
|
||||
Файл формата CSV с колонками: identity;secretKey
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button type="button" class="btn btn-primary w-100" onclick="uploadApiKey()">
|
||||
📤 Загрузить apiKey.csv
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<button type="button" class="btn btn-success w-100" onclick="saveServerUrl()" id="saveServerUrlButton">
|
||||
{% if data.url %}
|
||||
<i class="bi bi-arrow-repeat me-2"></i>Обновить URL
|
||||
{% else %}
|
||||
<i class="bi bi-save me-2"></i>Сохранить URL
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Аккордеон с запросами -->
|
||||
<div class="accordion mb-4" id="requestsAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#requestsCollapse" aria-expanded="false" aria-controls="requestsCollapse">
|
||||
⚙️ Настроенные запросы
|
||||
</button>
|
||||
</h2>
|
||||
<div id="requestsCollapse" class="accordion-collapse collapse" data-bs-parent="#requestsAccordion">
|
||||
<div class="accordion-body">
|
||||
<!-- Форма создания/редактирования запроса -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Добавить/Редактировать запрос</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="requestForm">
|
||||
<input type="hidden" id="requestId">
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Название запроса</label>
|
||||
<input type="text" class="form-control" id="title"
|
||||
placeholder="Получить список пользователей" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">HTTP метод</label>
|
||||
<select class="form-select" id="method" required>
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
<option value="DELETE">DELETE</option>
|
||||
<option value="PATCH">PATCH</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">URL путь</label>
|
||||
<input type="text" class="form-control" id="url_path" placeholder="/users"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Динамические параметры Query -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label fw-bold">Параметры Query</label>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="addQueryParam()">
|
||||
➕ Добавить параметр
|
||||
</button>
|
||||
</div>
|
||||
<div id="queryParamsContainer">
|
||||
<!-- Поля будут добавляться динамически -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Динамические параметры Payload -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label fw-bold">Параметры Payload (для POST/PUT/PATCH)</label>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="addPayloadParam()">
|
||||
➕ Добавить параметр
|
||||
</button>
|
||||
</div>
|
||||
<div id="payloadParamsContainer">
|
||||
<!-- Поля будут добавляться динамически -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-success" onclick="saveRequest()">
|
||||
💾 Сохранить запрос
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="resetForm()">
|
||||
🆕 Новый запрос
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 api-status">
|
||||
<div
|
||||
class="card-header bg-success bg-opacity-10 border-success d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="bi bi-key me-2"></i>API ключ</h5>
|
||||
<span id="apiKeyCheck" class="status-indicator">
|
||||
{% if data.apiKey %}
|
||||
<span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>Загружен</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning"><i class="bi bi-exclamation-triangle me-1"></i>Не загружен</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">CSV файл с ключами</label>
|
||||
<input type="file" class="form-control" id="api_key_file" accept=".csv">
|
||||
<div class="form-text small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Файл должен содержать колонки: <code>identity;secretKey</code>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary w-100" onclick="uploadApiKey()" id="uploadApiKeyButton">
|
||||
{% if data.apiKey %}
|
||||
<i class="bi bi-arrow-repeat me-2"></i>Обновить ключ
|
||||
{% else %}
|
||||
<i class="bi bi-upload me-2"></i>Загрузить API ключ
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Список существующих запросов -->
|
||||
<!-- Основной раздел: Запросы -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-dark bg-opacity-10">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0"><i class="bi bi-send me-2"></i>Управление запросами</h4>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="newRequest()">
|
||||
<i class="bi bi-plus-circle me-1"></i>Новый запрос
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="row g-0">
|
||||
<!-- Левая колонка: Список запросов -->
|
||||
<div class="col-md-4 border-end">
|
||||
<div class="p-3 border-bottom bg-light">
|
||||
<h5 class="mb-0"><i class="bi bi-list-ul me-2"></i>Сохраненные запросы</h5>
|
||||
</div>
|
||||
<div class="requests-list p-3">
|
||||
<div id="requestsList">
|
||||
<div class="text-center">
|
||||
<!-- Список запросов будет загружен здесь -->
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
@@ -140,575 +115,154 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Раздел выполнения запросов -->
|
||||
<div class="card">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h5 class="mb-0">📥 Выполнение запроса</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="executeForm">
|
||||
<div class="row g-3 align-items-end mb-4">
|
||||
<div class="col-md-10">
|
||||
<label class="form-label">Выберите запрос для выполнения</label>
|
||||
<select class="form-select" id="requestSelect" required>
|
||||
<option value="" disabled selected>Выберите запрос...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="button" class="btn btn-warning w-100" onclick="executeRequest()">
|
||||
🚀 Отправить запрос
|
||||
</button>
|
||||
</div>
|
||||
<!-- Правая колонка: Редактор запроса -->
|
||||
<div class="col-md-8">
|
||||
<div class="p-3 border-bottom bg-light">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-pencil-square me-2"></i>
|
||||
<span id="editorTitle">Создание нового запроса</span>
|
||||
</h5>
|
||||
</div>
|
||||
</form>
|
||||
<div class="p-4">
|
||||
<form id="requestForm">
|
||||
<input type="hidden" id="requestId">
|
||||
|
||||
<!-- Окно с результатом -->
|
||||
<div id="responseSection" style="display: none;">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6>Результат выполнения:</h6>
|
||||
<button type="button" class="btn btn-sm btn-outline-success" onclick="downloadResponse()">
|
||||
⬇️ Скачать JSON
|
||||
</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div id="responseContainer" class="response-container"
|
||||
style="max-height: 500px; overflow-y: auto;">
|
||||
<!-- Ответ будет отображен здесь -->
|
||||
<!-- Основные поля -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Название запроса</label>
|
||||
<input type="text" class="form-control form-control-lg" id="title"
|
||||
placeholder="Например: Получить список пользователей" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-semibold">HTTP метод</label>
|
||||
<select class="form-select form-select-lg" id="method" required>
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
<option value="DELETE">DELETE</option>
|
||||
<option value="PATCH">PATCH</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-semibold">URL путь</label>
|
||||
<input type="text" class="form-control form-control-lg" id="url_path"
|
||||
placeholder="/users" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Query параметры -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-body-secondary">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">
|
||||
<i class="bi bi-filter me-2"></i>Query параметры
|
||||
</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="addQueryParam()">
|
||||
<i class="bi bi-plus me-1"></i>Добавить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="queryParamsContainer" class="mb-2">
|
||||
<!-- Параметры будут добавляться сюда -->
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="addQueryParam()">
|
||||
<i class="bi bi-plus-circle me-1"></i>Добавить параметр Query
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payload параметры -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-body-secondary">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">
|
||||
<i class="bi bi-code me-2"></i>Body параметры
|
||||
</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="addPayloadParam()">
|
||||
<i class="bi bi-plus me-1"></i>Добавить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="payloadParamsContainer" class="mb-2">
|
||||
<!-- Параметры будут добавляться сюда -->
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="addPayloadParam()">
|
||||
<i class="bi bi-plus-circle me-1"></i>Добавить параметр Body
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-center align-items-center gap-2 text-muted d-none"
|
||||
id="timestampDiv">
|
||||
<div>
|
||||
<small>Создано: </small><span class="small" id="createdAt"></span>
|
||||
</div>
|
||||
<div>
|
||||
<small>Последнее обновление: </small><span class="small" id="updatedAt"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопки действий -->
|
||||
<div class="d-flex gap-3 justify-content-end pt-3 border-top">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="resetForm()">
|
||||
<i class="bi bi-x-lg me-1"></i>Сбросить
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveRequest()"
|
||||
id="saveRequestButton">
|
||||
<i class="bi bi-save me-1"></i>Сохранить запрос
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning" onclick="executeCurrentRequest()"
|
||||
id="executeButton" disabled>
|
||||
<i class="bi bi-play-fill me-1"></i>Запустить
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.response-container {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
background-color: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
<!-- Раздел с результатом -->
|
||||
<div class="card fade-in" id="responseCard" style="display: none;">
|
||||
<div class="card-header bg-info bg-opacity-10">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="bi bi-arrow-return-right me-2"></i>Результат выполнения запроса</h5>
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-outline-success me-2" onclick="downloadResponse()">
|
||||
<i class="bi bi-download me-1"></i>Скачать JSON
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleResponse()">
|
||||
<i class="bi bi-chevron-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body" id="responseBody">
|
||||
<div id="responseContainer" class="response-container">
|
||||
<!-- Ответ будет отображаться здесь -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
.json-key {
|
||||
color: #92278f;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.json-string {
|
||||
color: #3ab54a;
|
||||
}
|
||||
|
||||
.json-number {
|
||||
color: #25aae2;
|
||||
}
|
||||
|
||||
.json-boolean {
|
||||
color: #f98280;
|
||||
}
|
||||
|
||||
.json-null {
|
||||
color: #f1592a;
|
||||
}
|
||||
|
||||
.param-row {
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #0d6efd;
|
||||
}
|
||||
|
||||
.accordion-button:not(.collapsed) {
|
||||
background-color: #e7f1ff;
|
||||
color: #0c63e4;
|
||||
}
|
||||
</style>
|
||||
<!-- Всплывающие уведомления -->
|
||||
<div id="alertContainer" style="position: fixed; top: 80px; right: 20px; z-index: 1050;"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Сохранение URL сервера
|
||||
async function saveServerUrl() {
|
||||
const serverUrl = document.getElementById('server_url').value;
|
||||
|
||||
if (!serverUrl) {
|
||||
alert('Пожалуйста, введите URL сервера');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/settings/medods_url', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ url: serverUrl })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('URL сервера сохранен!');
|
||||
} else {
|
||||
const error = await response.text();
|
||||
alert('Ошибка сохранения: ' + error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
alert('Ошибка сохранения!');
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка API ключа
|
||||
async function uploadApiKey() {
|
||||
const fileInput = document.getElementById('api_key_file');
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (!file) {
|
||||
alert('Пожалуйста, выберите файл');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
const lines = text.split('\n');
|
||||
|
||||
if (lines.length < 2) {
|
||||
alert('Файл должен содержать заголовок и данные');
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = lines[0].split(';').map(h => h.trim());
|
||||
if (!headers.includes('identity') || !headers.includes('secretKey')) {
|
||||
alert('Файл должен содержать колонки: identity и secretKey');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {};
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
if (lines[i].trim()) {
|
||||
const values = lines[i].split(';').map(v => v.trim());
|
||||
if (values.length >= 2) {
|
||||
data[values[0]] = values[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch('/settings/medods_apikey', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('API ключ загружен!');
|
||||
fileInput.value = '';
|
||||
} else {
|
||||
const error = await response.text();
|
||||
alert('Ошибка загрузки: ' + error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
alert('Ошибка загрузки файла!');
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка запросов при раскрытии аккордеона
|
||||
document.getElementById('requestsAccordion').addEventListener('show.bs.collapse', function () {
|
||||
loadRequests();
|
||||
});
|
||||
|
||||
// Загрузка списка запросов
|
||||
async function loadRequests() {
|
||||
try {
|
||||
const response = await fetch('/settings/requests');
|
||||
const requests = await response.json();
|
||||
|
||||
// Обновляем выпадающий список для выполнения
|
||||
const select = document.getElementById('requestSelect');
|
||||
select.innerHTML = '<option value="" disabled selected>Выберите запрос...</option>';
|
||||
requests.forEach(req => {
|
||||
const option = document.createElement('option');
|
||||
option.value = req.id;
|
||||
option.textContent = `${req.id} - ${req.title}`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
// Отображаем список запросов
|
||||
const container = document.getElementById('requestsList');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (requests.length === 0) {
|
||||
container.innerHTML = '<div class="alert alert-info">Нет настроенных запросов</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
requests.forEach(request => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card mb-2';
|
||||
card.innerHTML = `
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="mb-1">${request.title}</h6>
|
||||
<small class="text-muted">
|
||||
${request.method} ${request.url_path}
|
||||
</small>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="editRequest(${request.id})">
|
||||
✏️
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteRequest(${request.id})">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(card);
|
||||
});
|
||||
|
||||
// Сохраняем запросы для использования
|
||||
window.requestsData = requests;
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки запросов:', error);
|
||||
document.getElementById('requestsList').innerHTML =
|
||||
'<div class="alert alert-danger">Ошибка загрузки запросов</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Добавление параметра Query
|
||||
function addQueryParam(key = '', value = '') {
|
||||
const container = document.getElementById('queryParamsContainer');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'row g-2 param-row';
|
||||
div.innerHTML = `
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control param-key" placeholder="Ключ" value="${key}">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control param-value" placeholder="Значение" value="${value}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger w-100" onclick="this.parentElement.parentElement.remove()">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
// Добавление параметра Payload
|
||||
function addPayloadParam(key = '', value = '') {
|
||||
const container = document.getElementById('payloadParamsContainer');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'row g-2 param-row';
|
||||
div.innerHTML = `
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control param-key" placeholder="Ключ" value="${key}">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control param-value" placeholder="Значение" value="${value}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger w-100" onclick="this.parentElement.parentElement.remove()">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
// Редактирование запроса
|
||||
function editRequest(id) {
|
||||
const request = window.requestsData.find(r => r.id === id);
|
||||
if (!request) return;
|
||||
|
||||
document.getElementById('requestId').value = request.id;
|
||||
document.getElementById('title').value = request.title;
|
||||
document.getElementById('method').value = request.method;
|
||||
document.getElementById('url_path').value = request.url_path;
|
||||
|
||||
// Очищаем контейнеры параметров
|
||||
document.getElementById('queryParamsContainer').innerHTML = '';
|
||||
document.getElementById('payloadParamsContainer').innerHTML = '';
|
||||
|
||||
// Добавляем query параметры
|
||||
if (request.query && typeof request.query === 'object') {
|
||||
Object.entries(request.query).forEach(([key, value]) => {
|
||||
addQueryParam(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
// Добавляем payload параметры
|
||||
if (request.payload && typeof request.payload === 'object') {
|
||||
Object.entries(request.payload).forEach(([key, value]) => {
|
||||
addPayloadParam(key, typeof value === 'object' ? JSON.stringify(value) : value);
|
||||
});
|
||||
}
|
||||
|
||||
// Прокручиваем к форме
|
||||
document.getElementById('requestForm').scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// Сброс формы
|
||||
function resetForm() {
|
||||
document.getElementById('requestForm').reset();
|
||||
document.getElementById('requestId').value = '';
|
||||
document.getElementById('queryParamsContainer').innerHTML = '';
|
||||
document.getElementById('payloadParamsContainer').innerHTML = '';
|
||||
}
|
||||
|
||||
// Сохранение запроса
|
||||
async function saveRequest() {
|
||||
const id = document.getElementById('requestId').value;
|
||||
const title = document.getElementById('title').value;
|
||||
const method = document.getElementById('method').value;
|
||||
const url_path = document.getElementById('url_path').value;
|
||||
|
||||
if (!title || !method || !url_path) {
|
||||
alert('Пожалуйста, заполните все обязательные поля');
|
||||
return;
|
||||
}
|
||||
|
||||
// Собираем query параметры
|
||||
const query = {};
|
||||
document.querySelectorAll('#queryParamsContainer .param-row').forEach(row => {
|
||||
const key = row.querySelector('.param-key').value;
|
||||
const value = row.querySelector('.param-value').value;
|
||||
if (key) query[key] = value;
|
||||
});
|
||||
|
||||
// Собираем payload параметры
|
||||
const payload = {};
|
||||
document.querySelectorAll('#payloadParamsContainer .param-row').forEach(row => {
|
||||
const key = row.querySelector('.param-key').value;
|
||||
const value = row.querySelector('.param-value').value;
|
||||
if (key) {
|
||||
// Пробуем парсить JSON, если это объект
|
||||
try {
|
||||
payload[key] = JSON.parse(value);
|
||||
} catch {
|
||||
payload[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const requestData = {
|
||||
title,
|
||||
method,
|
||||
url_path,
|
||||
query,
|
||||
payload
|
||||
};
|
||||
|
||||
if (id) {
|
||||
requestData.id = parseInt(id);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/settings/requests', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Запрос сохранен!');
|
||||
resetForm();
|
||||
loadRequests();
|
||||
} else {
|
||||
const error = await response.text();
|
||||
alert('Ошибка сохранения: ' + error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
alert('Ошибка сохранения!');
|
||||
}
|
||||
}
|
||||
|
||||
// Удаление запроса
|
||||
async function deleteRequest(id) {
|
||||
if (!confirm('Вы уверены, что хотите удалить этот запрос?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/settings/requests/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Запрос удален!');
|
||||
loadRequests();
|
||||
} else {
|
||||
const error = await response.text();
|
||||
alert('Ошибка удаления: ' + error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
alert('Ошибка удаления!');
|
||||
}
|
||||
}
|
||||
|
||||
// Выполнение запроса
|
||||
async function executeRequest() {
|
||||
const select = document.getElementById('requestSelect');
|
||||
const requestId = select.value;
|
||||
|
||||
if (!requestId) {
|
||||
alert('Пожалуйста, выберите запрос');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/settings/requests', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ id: parseInt(requestId) })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
displayResponse(data);
|
||||
document.getElementById('responseSection').style.display = 'block';
|
||||
|
||||
// Прокручиваем к результату
|
||||
document.getElementById('responseSection').scrollIntoView({ behavior: 'smooth' });
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
displayResponse({ error: error.message });
|
||||
document.getElementById('responseSection').style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Отображение ответа
|
||||
function displayResponse(data, container = document.getElementById('responseContainer'), level = 0) {
|
||||
container.innerHTML = '';
|
||||
|
||||
function formatValue(value, indent = 0) {
|
||||
const indentStr = ' '.repeat(indent);
|
||||
|
||||
if (value === null) {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'json-null';
|
||||
span.textContent = 'null';
|
||||
return span;
|
||||
} else if (typeof value === 'boolean') {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'json-boolean';
|
||||
span.textContent = value.toString();
|
||||
return span;
|
||||
} else if (typeof value === 'number') {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'json-number';
|
||||
span.textContent = value;
|
||||
return span;
|
||||
} else if (typeof value === 'string') {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'json-string';
|
||||
span.textContent = `"${value}"`;
|
||||
return span;
|
||||
} else if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
return document.createTextNode('[]');
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode('['));
|
||||
|
||||
value.forEach((item, index) => {
|
||||
const itemDiv = document.createElement('div');
|
||||
itemDiv.style.paddingLeft = '20px';
|
||||
itemDiv.appendChild(formatValue(item, indent + 1));
|
||||
if (index < value.length - 1) {
|
||||
itemDiv.appendChild(document.createTextNode(','));
|
||||
}
|
||||
div.appendChild(itemDiv);
|
||||
});
|
||||
|
||||
div.appendChild(document.createTextNode(']'));
|
||||
return div;
|
||||
} else if (typeof value === 'object') {
|
||||
const entries = Object.entries(value);
|
||||
if (entries.length === 0) {
|
||||
return document.createTextNode('{}');
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode('{'));
|
||||
|
||||
entries.forEach(([key, val], index) => {
|
||||
const itemDiv = document.createElement('div');
|
||||
itemDiv.style.paddingLeft = '20px';
|
||||
|
||||
const keySpan = document.createElement('span');
|
||||
keySpan.className = 'json-key';
|
||||
keySpan.textContent = `"${key}": `;
|
||||
itemDiv.appendChild(keySpan);
|
||||
|
||||
itemDiv.appendChild(formatValue(val, indent + 1));
|
||||
|
||||
if (index < entries.length - 1) {
|
||||
itemDiv.appendChild(document.createTextNode(','));
|
||||
}
|
||||
|
||||
div.appendChild(itemDiv);
|
||||
});
|
||||
|
||||
div.appendChild(document.createTextNode('}'));
|
||||
return div;
|
||||
}
|
||||
|
||||
return document.createTextNode(String(value));
|
||||
}
|
||||
|
||||
container.appendChild(formatValue(data));
|
||||
window.lastResponse = data;
|
||||
}
|
||||
|
||||
// Скачивание ответа
|
||||
function downloadResponse() {
|
||||
if (!window.lastResponse) return;
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const filename = `request_${timestamp}.json`;
|
||||
const jsonStr = JSON.stringify(window.lastResponse, null, 2);
|
||||
|
||||
const blob = new Blob([jsonStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function setServerUrlInput(data) {
|
||||
const serverUrlInput = document.getElementById('server_url');
|
||||
if (serverUrlInput && data) {
|
||||
serverUrlInput.value = data.url;
|
||||
serverUrlInput.disabled = true;
|
||||
}
|
||||
}
|
||||
// Инициализация
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Добавляем примеры параметров
|
||||
addQueryParam();
|
||||
addPayloadParam();
|
||||
setServerUrlInput(pageData);
|
||||
});
|
||||
const pageData = {{ data | tojson }};
|
||||
</script>
|
||||
<script src="/static/js/medods.js"></script>
|
||||
{% endblock %}
|
||||
+170
-15
@@ -1,22 +1,177 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}VK{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="/static/css/vk.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3 class="mb-4">📣 VK</h3>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Access Token группы</label>
|
||||
<input class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">ID сообщества</label>
|
||||
<input class="form-control">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary">Сохранить</button>
|
||||
<!-- Заголовок -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="mb-1"><i class="bi bi-megaphone vk-icon me-2"></i>VK Настройки</h2>
|
||||
<p class="text-muted mb-0">Настройки для работы с VK API и сообществом</p>
|
||||
</div>
|
||||
<div class="badge bg-primary fs-6 px-3 py-2">
|
||||
<i class="bi bi-bell me-1"></i>Социальная сеть
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Настройки VK -->
|
||||
<div class="row">
|
||||
<div class="col-lg-8 mx-auto">
|
||||
<div class="card setting-card fade-in">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="bi bi-gear vk-icon me-2"></i>Настройки VK</h5>
|
||||
<span id="vkStatus" class="status-badge">
|
||||
{% if data.vk_settings %}
|
||||
<span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>Настроено</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning"><i class="bi bi-exclamation-triangle me-1"></i>Не
|
||||
настроено</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="vkForm">
|
||||
<!-- Access Token группы -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold">
|
||||
<i class="bi bi-key vk-icon me-1"></i>Access Token группы
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light"><i class="bi bi-shield-lock"></i></span>
|
||||
<input type="password" class="form-control" id="access_token"
|
||||
placeholder="Введите Access Token вашей группы"
|
||||
value="{{ data.vk_settings.access_token if data.vk_settings else '' }}">
|
||||
<button class="btn btn-outline-secondary" type="button"
|
||||
onclick="togglePassword('access_token')">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Access Token с правами: groups, wall
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ID сообщества -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold">
|
||||
<i class="bi bi-people vk-icon me-1"></i>ID сообщества
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light"><i class="bi bi-hash"></i></span>
|
||||
<input type="number" class="form-control" id="group_id" placeholder="Например: 123456789"
|
||||
step="1" min="0" value="{{ data.vk_settings.group_id if data.vk_settings else '' }}">
|
||||
</div>
|
||||
<div class="form-text small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Числовой ID вашего сообщества VK (без знака минус)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ID Базового фото -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold">
|
||||
<i class="bi bi-image vk-icon me-1"></i>ID Базового фото
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light"><i class="bi bi-image-alt"></i></span>
|
||||
<input type="number" class="form-control" id="base_photo_url"
|
||||
placeholder="Например: 12345689123" step="1" min="0"
|
||||
value="{{ data.vk_settings.base_photo_url if data.vk_settings else '' }}">
|
||||
</div>
|
||||
<div class="form-text small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
ID фото в формате: <code>photo_id</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Информационная панель -->
|
||||
<div class="alert alert-info">
|
||||
<div class="d-flex">
|
||||
<div class="me-3">
|
||||
<i class="bi bi-lightbulb fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="alert-heading">Как получить данные:</h6>
|
||||
<ul class="mb-0 small">
|
||||
<li><strong>Access Token:</strong> Создайте Standalone-приложение в <a
|
||||
href="https://vk.com/apps?act=manage" target="_blank">управлении
|
||||
приложениями VK</a></li>
|
||||
<li><strong>ID сообщества:</strong> Число в адресе сообщества после
|
||||
<code>vk.com/public</code> или <code>vk.com/club</code>
|
||||
</li>
|
||||
<li><strong>ID Базового фото:</strong> Загрузите фото в альбом сообщества и
|
||||
скопируйте ID из адреса фото</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопки действий -->
|
||||
<div class="d-flex gap-3 pt-3 border-top">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="resetForm()">
|
||||
<i class="bi bi-x-lg me-1"></i>Сбросить
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveVkSettings()" id="saveButton">
|
||||
<i class="bi bi-save me-1"></i>Сохранить настройки
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Информация о текущих настройках -->
|
||||
{% if data.vk_settings %}
|
||||
<div class="card mt-4 fade-in">
|
||||
<div class="card-header bg-success bg-opacity-10 text-success">
|
||||
<h6 class="mb-0"><i class="bi bi-check-circle me-2"></i>Текущие настройки</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title"><i class="bi bi-key me-2"></i>Access Token</h6>
|
||||
<p class="card-text small text-truncate" id="tokenPreview">
|
||||
{{ data.vk_settings.access_token[:15] }}...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title"><i class="bi bi-people me-2"></i>ID сообщества</h6>
|
||||
<p class="card-text">-{{ data.vk_settings.group_id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title"><i class="bi bi-image me-2"></i>ID Базового фото</h6>
|
||||
<p class="card-text">photo-{{ data.vk_settings.group_id }}_{{
|
||||
data.vk_settings.base_photo_url }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Контейнер для уведомлений -->
|
||||
<div id="alertContainer" class="alert-fixed"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const pageData = {{ data | tojson }};
|
||||
</script>
|
||||
<script src="/static/js/vk.js"></script>
|
||||
{% endblock %}
|
||||
+7
-3
@@ -2,6 +2,8 @@ import csv
|
||||
import jwt
|
||||
import time
|
||||
|
||||
from db import MedodsAPI
|
||||
|
||||
|
||||
def load_api_key(csv_path="apiKey.csv"):
|
||||
with open(csv_path, newline="", encoding="utf-8") as f:
|
||||
@@ -10,10 +12,12 @@ def load_api_key(csv_path="apiKey.csv"):
|
||||
|
||||
|
||||
def generate_token():
|
||||
data = load_api_key()
|
||||
medodsDB = MedodsAPI.query.first()
|
||||
if not medodsDB:
|
||||
return None
|
||||
|
||||
identity = data["identity"]
|
||||
secret = data["secret"]
|
||||
identity = medodsDB.identity
|
||||
secret = medodsDB.secretKey
|
||||
|
||||
iat = int(time.time())
|
||||
exp = iat + 60 # <= 64 сек
|
||||
|
||||
Reference in New Issue
Block a user