базовые настройки

This commit is contained in:
2025-12-19 01:01:35 +03:00
parent 7334aae92e
commit ad577b1f4d
13 changed files with 1670 additions and 819 deletions
BIN
View File
Binary file not shown.
+146 -59
View File
@@ -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)
+54 -18
View File
@@ -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
View File
@@ -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
+13
View File
@@ -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;
}
+94
View File
@@ -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); }
}
+63
View File
@@ -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;
}
+668
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 сек