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

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.
+145 -58
View File
@@ -1,8 +1,7 @@
from flask import Flask, request, jsonify from flask import Flask, request, jsonify
from config import Config from config import Config
from db import MedodsAPI, db, VkPost from db import VkAPI, db, MedodsAPI, ApiEndpoint
from scheduler import start_scheduler from scheduler import start_scheduler
from token_utils import generate_token
from http_client import send_request from http_client import send_request
import logging import logging
import os import os
@@ -33,16 +32,28 @@ def index():
return render_template("index.html") return render_template("index.html")
@app.route("/medods") @app.route("/medods", methods=["GET"])
def medods(): def medods():
medods_api = MedodsAPI.query.first() 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) return render_template("medods.html", data=data)
@app.route("/vk") @app.route("/vk")
def 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") @app.route("/posts")
@@ -57,66 +68,153 @@ def init():
logger.info("Приложение запущено") logger.info("Приложение запущено")
@app.route("/settings/medods_url", methods=["POST"]) @app.route("/settings/medods", methods=["POST"])
def medods_url(): def medods_url():
data = request.json data = request.json
db.session.merge(MedodsAPI(url=data.get("url", "http://10.3.10.10/api/v2/"))) logger.info(data)
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() db.session.commit()
logger.info(data) logger.info("Обновлен url")
return jsonify({"status": "ok"}) 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"]) @app.route("/settings/requests", methods=["GET", "POST", "PATCH", "DELETE"])
def medods_apikey():
data = request.json
logger.info(data)
return jsonify({"status": "ok"})
@app.route("/settings/requests", methods=["GET", "POST", "PATCH"])
def get_requests(): def get_requests():
requestData = (
request.json if request.method in ["POST", "PATCH", "DELETE"] else None
)
match request.method: match request.method:
case "DELETE":
logger.info("Удален запрос")
logger.info(requestData)
return jsonify({"status": "ok"})
case "POST": case "POST":
logger.info("Добавлен новый запрос") logger.info("Добавлен/обновлен запрос")
logger.info(request.json) if "id" in requestData:
return jsonify({"status": "ok"})
case "PATCH":
logger.info("Обновлен запрос") logger.info("Обновлен запрос")
logger.info(request.json) 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"}) 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("Выполнен запрос")
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": case "GET":
logger.info("Получен список запросов") logger.info("Получен список запросов")
requestsDB = ApiEndpoint.query.all()
requestsList = [r.toDict() for r in requestsDB]
logger.info(requestsList)
return jsonify( return jsonify(
[
{ {
"id": 1, "status": "ok",
"method": "GET", "requests": requestsList,
"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": {},
},
]
) )
case _: case _:
logger.error("Неверный метод запроса") logger.error("Неверный метод запроса")
return jsonify({"status": "error"}) return jsonify({"status": "error"}), 405
@app.route("/token", methods=["GET"]) @app.route("/settings/vk", methods=["POST"])
def token(): def settings_vk():
return jsonify({"token": generate_token()}) 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"]) @app.route("/request", methods=["POST"])
@@ -128,16 +226,5 @@ def make_request():
return jsonify({"status": response.status_code}) 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__": if __name__ == "__main__":
app.run(debug=True) app.run(debug=True)
+54 -18
View File
@@ -4,28 +4,64 @@ from datetime import datetime
db = SQLAlchemy() 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): class MedodsAPI(db.Model):
__tablename__ = "medods_api"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
url = db.Column(db.Text) url = db.Column(db.Text)
identity = db.Column(db.Text) identity = db.Column(db.Text)
secretKey = db.Column(db.Text) secretKey = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.now) created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=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"),
}
+19 -12
View File
@@ -1,20 +1,27 @@
import requests import requests
from db import db, HttpRequestLog
import json from token_utils import generate_token
def send_request(method, url, data=None, headers=None): def send_request(method, url, json_data=None, params=None):
response = requests.request(method=method, url=url, json=data, headers=headers) from app import logger
log = HttpRequestLog( headers = {"Content-Type": "application/json"}
method=method, bearer_token = generate_token()
url=url, if bearer_token:
request_data=json.dumps(data, ensure_ascii=False), headers["Authorization"] = f"Bearer {bearer_token}"
response_code=response.status_code, logger.info(headers)
response_body=response.text,
try:
response = requests.request(
method=method, url=url, params=params, json=json_data, headers=headers
) )
except Exception as e:
logger.error(f"Ошибка при выполнении запроса: {e}")
return
db.session.add(log) logger.info(response.status_code)
db.session.commit() logger.info(response.headers)
# logger.info(response.text)
return response 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)');
}
});
+48 -32
View File
@@ -3,55 +3,71 @@
<head> <head>
<meta charset="utf-8"> <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"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
body { <link href="/static/css/base.css" rel="stylesheet">
padding-top: 60px; {% block styles %}{% endblock %}
}
.navbar-brand {
font-weight: 600;
}
pre {
background: #f8f9fa;
padding: 12px;
border-radius: 6px;
}
</style>
</head> </head>
<body> <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"> <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"> <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> </button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/">Главная</a></li> <!-- Меню -->
<li><a class="dropdown-item" href="/medods">Medods</a></li> <div class="collapse navbar-collapse" id="mainNavbar">
<li><a class="dropdown-item" href="/vk">VK</a></li> <ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li><a class="dropdown-item" href="/posts">Посты</a></li>
<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> </ul>
</div> </div>
</div> </div>
</nav> </nav>
<script> <main class="container-fluid pt-2">
const pageData = {{ data | tojson }};
</script>
<div class="container-fluid">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body> </body>
</html> </html>
+194 -640
View File
@@ -1,81 +1,143 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Medods{% endblock %} {% block title %}Medods{% endblock %}
{% block styles %}
<link rel="stylesheet" href="/static/css/medods.css">
{% endblock %}
{% block content %} {% 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="row mb-4">
<div class="card-header bg-primary text-white"> <div class="col-lg-6 mb-4 mb-lg-0">
<h5 class="mb-0">Подключение к серверу</h5> <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>
<div class="card-body"> <div class="card-body">
<form id="serverForm"> <div class="mb-3">
<div class="row g-3 align-items-end"> <label class="form-label fw-semibold">URL адрес сервера</label>
<div class="col-md-9"> <div class="input-group">
<label class="form-label">URL адрес сервера</label> <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" <input type="text" class="form-control" id="server_url" placeholder="https://api.example.com"
required> value="{{ data.url or '' }}">
</div> </div>
<div class="col-md-3"> <div class="form-text small">Введите полный URL вашего сервера Medods API</div>
<button type="button" class="btn btn-success w-100" onclick="saveServerUrl()"> </div>
💾 Сохранить <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> </button>
</div> </div>
</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>
</div>
</div> </div>
<!-- Аккордеон с запросами --> <div class="col-lg-6">
<div class="accordion mb-4" id="requestsAccordion"> <div class="card h-100 api-status">
<div class="accordion-item"> <div
<h2 class="accordion-header"> class="card-header bg-success bg-opacity-10 border-success d-flex justify-content-between align-items-center">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" <h5 class="mb-0"><i class="bi bi-key me-2"></i>API ключ</h5>
data-bs-target="#requestsCollapse" aria-expanded="false" aria-controls="requestsCollapse"> <span id="apiKeyCheck" class="status-indicator">
⚙️ Настроенные запросы {% if data.apiKey %}
</button> <span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>Загружен</span>
</h2> {% else %}
<div id="requestsCollapse" class="accordion-collapse collapse" data-bs-parent="#requestsAccordion"> <span class="badge bg-warning"><i class="bi bi-exclamation-triangle me-1"></i>Не загружен</span>
<div class="accordion-body"> {% endif %}
<!-- Форма создания/редактирования запроса --> </span>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Добавить/Редактировать запрос</h5>
</div> </div>
<div class="card-body"> <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 py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
</div>
</div>
</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>
<div class="p-4">
<form id="requestForm"> <form id="requestForm">
<input type="hidden" id="requestId"> <input type="hidden" id="requestId">
<div class="row g-3 mb-3"> <!-- Основные поля -->
<div class="row g-3 mb-4">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Название запроса</label> <label class="form-label fw-semibold">Название запроса</label>
<input type="text" class="form-control" id="title" <input type="text" class="form-control form-control-lg" id="title"
placeholder="Получить список пользователей" required> placeholder="Например: Получить список пользователей" required>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">HTTP метод</label> <label class="form-label fw-semibold">HTTP метод</label>
<select class="form-select" id="method" required> <select class="form-select form-select-lg" id="method" required>
<option value="GET">GET</option> <option value="GET">GET</option>
<option value="POST">POST</option> <option value="POST">POST</option>
<option value="PUT">PUT</option> <option value="PUT">PUT</option>
@@ -84,631 +146,123 @@
</select> </select>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">URL путь</label> <label class="form-label fw-semibold">URL путь</label>
<input type="text" class="form-control" id="url_path" placeholder="/users" <input type="text" class="form-control form-control-lg" id="url_path"
required> placeholder="/users" required>
</div> </div>
</div> </div>
<!-- Динамические параметры Query --> <!-- Query параметры -->
<div class="mb-4"> <div class="card mb-4">
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="card-header bg-body-secondary">
<label class="form-label fw-bold">Параметры Query</label> <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" <button type="button" class="btn btn-sm btn-outline-primary"
onclick="addQueryParam()"> 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> </button>
</div> </div>
<div id="queryParamsContainer">
<!-- Поля будут добавляться динамически -->
</div> </div>
</div> </div>
<!-- Динамические параметры Payload --> <!-- Payload параметры -->
<div class="mb-4"> <div class="card mb-4">
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="card-header bg-body-secondary">
<label class="form-label fw-bold">Параметры Payload (для POST/PUT/PATCH)</label> <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" <button type="button" class="btn btn-sm btn-outline-primary"
onclick="addPayloadParam()"> onclick="addPayloadParam()">
➕ Добавить параметр <i class="bi bi-plus me-1"></i>Добавить
</button> </button>
</div> </div>
<div id="payloadParamsContainer">
<!-- Поля будут добавляться динамически -->
</div> </div>
<div class="card-body">
<div id="payloadParamsContainer" class="mb-2">
<!-- Параметры будут добавляться сюда -->
</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>
<!-- Список существующих запросов -->
<div id="requestsList">
<div class="text-center"> <div class="text-center">
<div class="spinner-border text-primary" role="status"> <button type="button" class="btn btn-sm btn-outline-primary"
<span class="visually-hidden">Загрузка...</span> onclick="addPayloadParam()">
</div> <i class="bi bi-plus-circle me-1"></i>Добавить параметр Body
</button>
</div> </div>
</div> </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>
<div>
<small>Последнее обновление: </small><span class="small" id="updatedAt"></span>
</div> </div>
</div> </div>
<!-- Раздел выполнения запросов --> <!-- Кнопки действий -->
<div class="card"> <div class="d-flex gap-3 justify-content-end pt-3 border-top">
<div class="card-header bg-info text-white"> <button type="button" class="btn btn-outline-secondary" onclick="resetForm()">
<h5 class="mb-0">📥 Выполнение запроса</h5> <i class="bi bi-x-lg me-1"></i>Сбросить
</div> </button>
<div class="card-body"> <button type="button" class="btn btn-primary" onclick="saveRequest()"
<form id="executeForm"> id="saveRequestButton">
<div class="row g-3 align-items-end mb-4"> <i class="bi bi-save me-1"></i>Сохранить запрос
<div class="col-md-10"> </button>
<label class="form-label">Выберите запрос для выполнения</label> <button type="button" class="btn btn-warning" onclick="executeCurrentRequest()"
<select class="form-select" id="requestSelect" required> id="executeButton" disabled>
<option value="" disabled selected>Выберите запрос...</option> <i class="bi bi-play-fill me-1"></i>Запустить
</select>
</div>
<div class="col-md-2">
<button type="button" class="btn btn-warning w-100" onclick="executeRequest()">
🚀 Отправить запрос
</button> </button>
</div>
</div> </div>
</form> </form>
<!-- Окно с результатом -->
<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>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<style> <!-- Раздел с результатом -->
.response-container { <div class="card fade-in" id="responseCard" style="display: none;">
font-family: 'Courier New', monospace; <div class="card-header bg-info bg-opacity-10">
font-size: 14px;
line-height: 1.4;
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
}
.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>
<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 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> <div>
<h6 class="mb-1">${request.title}</h6> <button type="button" class="btn btn-sm btn-outline-success me-2" onclick="downloadResponse()">
<small class="text-muted"> <i class="bi bi-download me-1"></i>Скачать JSON
${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>
<button class="btn btn-sm btn-outline-danger" onclick="deleteRequest(${request.id})"> <button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleResponse()">
🗑️ <i class="bi bi-chevron-up"></i>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
`; <div class="card-body" id="responseBody">
container.appendChild(card); <div id="responseContainer" class="response-container">
}); <!-- Ответ будет отображаться здесь -->
// Сохраняем запросы для использования
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>
<div class="col-md-5">
<input type="text" class="form-control param-value" placeholder="Значение" value="${value}">
</div> </div>
<div class="col-md-2"> </div>
<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 = '') { <div id="alertContainer" style="position: fixed; top: 80px; right: 20px; z-index: 1050;"></div>
const container = document.getElementById('payloadParamsContainer'); {% endblock %}
const div = document.createElement('div');
div.className = 'row g-2 param-row'; {% block scripts %}
div.innerHTML = ` <script>
<div class="col-md-5"> const pageData = {{ data | tojson }};
<input type="text" class="form-control param-key" placeholder="Ключ" value="${key}"> </script>
</div> <script src="/static/js/medods.js"></script>
<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);
});
</script>
{% endblock %} {% endblock %}
+169 -14
View File
@@ -1,22 +1,177 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}VK{% endblock %} {% block title %}VK{% endblock %}
{% block styles %}
<link rel="stylesheet" href="/static/css/vk.css">
{% endblock %}
{% block content %} {% block content %}
<h3 class="mb-4">📣 VK</h3> <!-- Заголовок -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="card"> <div>
<div class="card-body"> <h2 class="mb-1"><i class="bi bi-megaphone vk-icon me-2"></i>VK Настройки</h2>
<div class="mb-3"> <p class="text-muted mb-0">Настройки для работы с VK API и сообществом</p>
<label class="form-label">Access Token группы</label>
<input class="form-control">
</div> </div>
<div class="badge bg-primary fs-6 px-3 py-2">
<div class="mb-3"> <i class="bi bi-bell me-1"></i>Социальная сеть
<label class="form-label">ID сообщества</label>
<input class="form-control">
</div>
<button class="btn btn-primary">Сохранить</button>
</div> </div>
</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 %} {% endblock %}
+7 -3
View File
@@ -2,6 +2,8 @@ import csv
import jwt import jwt
import time import time
from db import MedodsAPI
def load_api_key(csv_path="apiKey.csv"): def load_api_key(csv_path="apiKey.csv"):
with open(csv_path, newline="", encoding="utf-8") as f: 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(): def generate_token():
data = load_api_key() medodsDB = MedodsAPI.query.first()
if not medodsDB:
return None
identity = data["identity"] identity = medodsDB.identity
secret = data["secret"] secret = medodsDB.secretKey
iat = int(time.time()) iat = int(time.time())
exp = iat + 60 # <= 64 сек exp = iat + 60 # <= 64 сек