From ad577b1f4df05b95df5ce0ef4e2f72b4b2087c40 Mon Sep 17 00:00:00 2001 From: Macbook Date: Fri, 19 Dec 2025 01:01:35 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B1=D0=B0=D0=B7=D0=BE=D0=B2=D1=8B=D0=B5=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.db | Bin 16384 -> 20480 bytes app.py | 205 +++++++--- db.py | 72 +++- http_client.py | 33 +- static/css/base.css | 13 + static/css/medods.css | 94 +++++ static/css/vk.css | 63 +++ static/js/medods.js | 668 +++++++++++++++++++++++++++++++ static/js/vk.js | 154 +++++++ templates/base.html | 82 ++-- templates/medods.html | 910 +++++++++++------------------------------- templates/vk.html | 185 ++++++++- token_utils.py | 10 +- 13 files changed, 1670 insertions(+), 819 deletions(-) create mode 100644 static/css/base.css create mode 100644 static/css/medods.css create mode 100644 static/css/vk.css create mode 100644 static/js/medods.js create mode 100644 static/js/vk.js diff --git a/app.db b/app.db index 04be550b21bf2a4e030e71a22bb124150c1f4f78..67170a1d1f3d87a19f0e3141868e522f9313b5de 100644 GIT binary patch delta 534 zcmZo@U~E{xI6+!am4ShQ6^NODm=TD%C+ZlBsxs*H{@~?{V_@dl&A>aI?+(vyo{Ei) z?mTvlk*w_E%F2vQl_iNuIjLpY@x`enC7F5Y#Yjw6=O9SxT-{xRbQHKa6O)rui;Lq+^0QO(6hd4hLO_B^iN&e$1sVAz`SGPi zIWXmurTAPY&)_}A0(5NM#Ks~YHdZzulYya0k%e7cU7fLo5$v?Yg3S2Typ)3c%)F9f z6b>_r^H8Kf4y?~jEy>7FQ3!Jka`tcx(l9j8RPgf;QSb}(@c}ujB(o$Z6)tOJYKkfi zbZUG-Vo3&W6$OcvIr)hx3SPnfejo!2OH+#~fqIG(bBlq3O#XgAKe`BjJp~~^5yZM# zP~biP;`_d5=Kr2VNr3xct=Nf*N~9Z;u8Im1h7~Xm^2g=1jm$;dIWaR*S4jeje@FuyR^WM-fp0P)b1AOHXW delta 488 zcmZozz}V2hI6+#Fg@J(qgkgYrqK>gJ3xi&pCNEHknRhw^KPO)t@AS=r0;;@|r}G{+ zYSdt27nhc1Y*H^tOv*_u%Z@L|FD`*n%+5isjv=lJA&yQyt_o1W$qM|^9wn(2B?=*~ z5g|GXT%4J?iRr2F1&JjYaG`?Iq@2v+jQGS71s9+RA)dajAeB(z)D#6Le}5lWN59F9 zyy8A3jO^mNx{S@zU?*ghloZ4lr52W^7MH~5sE(>&>ZUD%t(xM!=Yhb3O zB$gy12^JUR=M|^MC+DZ6DtP*ZxVpOrfpiw7LM4*&Q!2sAC-32xp8Sqqhz%5q3FXOB=o#w)2}1+@ p#DYxyG9!H>10z#iLnB>7a|I(qD^mk26Ei(SV-quDV_Y&OMgWBQhv@(S diff --git a/app.py b/app.py index eb416ce..1ec9be3 100644 --- a/app.py +++ b/app.py @@ -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) diff --git a/db.py b/db.py index 765906f..f936789 100644 --- a/db.py +++ b/db.py @@ -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"" + + 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"), + } diff --git a/http_client.py b/http_client.py index aed10af..9c69a8a 100644 --- a/http_client.py +++ b/http_client.py @@ -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 diff --git a/static/css/base.css b/static/css/base.css new file mode 100644 index 0000000..e6847a8 --- /dev/null +++ b/static/css/base.css @@ -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; +} \ No newline at end of file diff --git a/static/css/medods.css b/static/css/medods.css new file mode 100644 index 0000000..84e6faa --- /dev/null +++ b/static/css/medods.css @@ -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); } +} \ No newline at end of file diff --git a/static/css/vk.css b/static/css/vk.css new file mode 100644 index 0000000..eb15bdc --- /dev/null +++ b/static/css/vk.css @@ -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; +} \ No newline at end of file diff --git a/static/js/medods.js b/static/js/medods.js new file mode 100644 index 0000000..f800519 --- /dev/null +++ b/static/js/medods.js @@ -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 = 'Настроен'; + saveServerUrlButton.innerHTML = 'Обновить URL'; + saveServerUrlButton.classList.remove('btn-success'); + saveServerUrlButton.classList.add('btn-outline-success'); + } else { + urlCheck.innerHTML = 'Не настроен'; + saveServerUrlButton.innerHTML = 'Сохранить URL'; + saveServerUrlButton.classList.remove('btn-outline-success'); + saveServerUrlButton.classList.add('btn-success'); + } + + // API Key индикатор + if (pageData && pageData.apiKey) { + apiKeyCheck.innerHTML = 'Загружен'; + uploadApiKeyButton.innerHTML = 'Обновить ключ'; + uploadApiKeyButton.classList.remove('btn-primary'); + uploadApiKeyButton.classList.add('btn-outline-primary'); + } else { + apiKeyCheck.innerHTML = 'Не загружен'; + uploadApiKeyButton.innerHTML = 'Загрузить 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 = ` +
+ + Нет сохраненных запросов
+ Нажмите "Новый запрос" для создания +
+ `; + return; + } + + let html = '
'; + + requestsData.forEach(request => { + const methodClass = getMethodClass(request.method); + const isActive = currentRequestId === request.id; + + html += ` +
+
+
+
+ ${request.method} + ${request.title} +
+ ${request.url_path} +
+
+ + +
+
+
+ `; + }); + + html += '
'; + 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 = 'Сохранить запрос'; + 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 = 'Обновить запрос'; + 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 = ` +
+
+
+ +
+
+ +
+
+ +
+
+
+ `; + + container.insertAdjacentHTML('beforeend', html); +} + +// Добавление параметра Payload +function addPayloadParam(key = '', value = '') { + const container = document.getElementById('payloadParamsContainer'); + const paramId = Date.now() + Math.random(); + + const html = ` +
+
+
+ +
+
+ +
+
+ +
+
+
+ `; + + 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 = 'Сохранить запрос'; + 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 = ` +
+ +
${message}
+ +
+ `; + + 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 = 'Выполняется...'; + executeButton.disabled = true; + } else { + executeButton.innerHTML = 'Запустить'; + executeButton.disabled = !currentRequestId; + } +} \ No newline at end of file diff --git a/static/js/vk.js b/static/js/vk.js new file mode 100644 index 0000000..a17a3a5 --- /dev/null +++ b/static/js/vk.js @@ -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 = 'Настроено'; + saveButton.innerHTML = 'Обновить настройки'; + saveButton.classList.remove('btn-primary'); + saveButton.classList.add('btn-outline-primary'); + } else { + vkStatus.innerHTML = 'Не настроено'; + saveButton.innerHTML = 'Сохранить настройки'; + 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 = ` +
+ +
${message}
+ +
+ `; + + 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)'); + } +}); \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index a2172e5..89ac764 100644 --- a/templates/base.html +++ b/templates/base.html @@ -3,55 +3,71 @@ - {% block title %}Система{% endblock %} + {% block title %}Control Panel{% endblock %} + + - + + + {% block styles %}{% endblock %} -