236 lines
7.5 KiB
Python
236 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
||
"""Core configuration REST API (buttons, layout, background, feedback,
|
||
settings, icons) for the ButtonTask web configurator."""
|
||
import uuid
|
||
from pathlib import Path
|
||
|
||
from flask import Blueprint, abort, jsonify, request, send_from_directory
|
||
|
||
from auth import login_required
|
||
from config_store import (ALLOWED_ICON_EXT, icons_dir, list_icons, load_config,
|
||
save_config)
|
||
|
||
api_bp = Blueprint("api", __name__)
|
||
|
||
|
||
@api_bp.route("/icons/<path:fname>")
|
||
@login_required
|
||
def serve_icon(fname):
|
||
return send_from_directory(str(icons_dir()), fname)
|
||
|
||
|
||
# --- config ---
|
||
@api_bp.route("/api/config", methods=["GET"])
|
||
@login_required
|
||
def api_get_config():
|
||
return jsonify(load_config())
|
||
|
||
|
||
@api_bp.route("/api/config", methods=["PUT"])
|
||
@login_required
|
||
def api_put_config():
|
||
data = request.get_json(force=True, silent=True)
|
||
if not isinstance(data, dict):
|
||
abort(400, "JSON object required")
|
||
save_config(data)
|
||
return jsonify({"ok": True})
|
||
|
||
|
||
# --- buttons ---
|
||
@api_bp.route("/api/buttons", methods=["POST"])
|
||
@login_required
|
||
def api_add_button():
|
||
data = request.get_json(force=True, silent=True) or {}
|
||
cfg = load_config()
|
||
btn = {
|
||
"id": data.get("id") or uuid.uuid4().hex,
|
||
"label": data.get("label", "Кнопка"),
|
||
"iconPath": data.get("iconPath", ""),
|
||
"action": {
|
||
"type": data.get("actionType", "http_get"),
|
||
"url": data.get("url", ""),
|
||
"headers": data.get("headers", {}),
|
||
"body": data.get("body", ""),
|
||
},
|
||
"feedback": {
|
||
"successText": data.get("successText", ""),
|
||
"errorText": data.get("errorText", "Ошибка"),
|
||
"pendingText": data.get("pendingText", "..."),
|
||
"fadeMs": int(data.get("fadeMs", 5000)),
|
||
},
|
||
"trigger": {
|
||
"mode": "click" if data.get("triggerMode") == "click" else "hold",
|
||
"holdMs": int(data.get("holdMs", 800)),
|
||
},
|
||
"color": data.get("color", "#7b007b"),
|
||
}
|
||
cfg.setdefault("buttons", []).append(btn)
|
||
save_config(cfg)
|
||
return jsonify(btn)
|
||
|
||
|
||
@api_bp.route("/api/buttons/<bid>", methods=["PUT"])
|
||
@login_required
|
||
def api_update_button(bid):
|
||
data = request.get_json(force=True, silent=True) or {}
|
||
cfg = load_config()
|
||
for b in cfg.get("buttons", []):
|
||
if b.get("id") == bid:
|
||
b["label"] = data.get("label", b.get("label"))
|
||
b["iconPath"] = data.get("iconPath", b.get("iconPath"))
|
||
action = b.setdefault("action", {})
|
||
action["type"] = data.get("actionType", action.get("type", "http_get"))
|
||
action["url"] = data.get("url", action.get("url", ""))
|
||
if "headers" in data:
|
||
action["headers"] = data["headers"]
|
||
if "body" in data:
|
||
action["body"] = data["body"]
|
||
feedback = b.setdefault("feedback", {})
|
||
feedback["successText"] = data.get("successText", feedback.get("successText", ""))
|
||
feedback["errorText"] = data.get("errorText", feedback.get("errorText", "Ошибка"))
|
||
feedback["pendingText"] = data.get("pendingText", feedback.get("pendingText", "..."))
|
||
feedback["fadeMs"] = int(data.get("fadeMs", feedback.get("fadeMs", 5000)))
|
||
trigger = b.setdefault("trigger", {})
|
||
if "triggerMode" in data:
|
||
trigger["mode"] = "click" if data["triggerMode"] == "click" else "hold"
|
||
else:
|
||
trigger.setdefault("mode", "hold")
|
||
if "holdMs" in data:
|
||
trigger["holdMs"] = int(data["holdMs"])
|
||
else:
|
||
trigger.setdefault("holdMs", 800)
|
||
if "color" in data:
|
||
b["color"] = data["color"]
|
||
save_config(cfg)
|
||
return jsonify(b)
|
||
abort(404)
|
||
|
||
|
||
@api_bp.route("/api/buttons/<bid>", methods=["DELETE"])
|
||
@login_required
|
||
def api_delete_button(bid):
|
||
cfg = load_config()
|
||
cfg["buttons"] = [b for b in cfg.get("buttons", []) if b.get("id") != bid]
|
||
save_config(cfg)
|
||
return jsonify({"ok": True})
|
||
|
||
|
||
@api_bp.route("/api/buttons/reorder", methods=["POST"])
|
||
@login_required
|
||
def api_reorder_buttons():
|
||
data = request.get_json(force=True, silent=True) or {}
|
||
order = data.get("order", [])
|
||
cfg = load_config()
|
||
by_id = {b.get("id"): b for b in cfg.get("buttons", [])}
|
||
new_list = [by_id[i] for i in order if i in by_id]
|
||
for b in cfg.get("buttons", []):
|
||
if b not in new_list:
|
||
new_list.append(b)
|
||
cfg["buttons"] = new_list
|
||
save_config(cfg)
|
||
return jsonify({"ok": True, "count": len(new_list)})
|
||
|
||
|
||
# --- layout / background / feedback / settings ---
|
||
@api_bp.route("/api/layout", methods=["PUT"])
|
||
@login_required
|
||
def api_set_layout():
|
||
data = request.get_json(force=True, silent=True) or {}
|
||
cfg = load_config()
|
||
layout = cfg.setdefault("layout", {})
|
||
for k in ("mode", "columns", "spacing", "showLabels"):
|
||
if k in data:
|
||
layout[k] = data[k]
|
||
save_config(cfg)
|
||
return jsonify(layout)
|
||
|
||
|
||
@api_bp.route("/api/background", methods=["PUT"])
|
||
@login_required
|
||
def api_set_background():
|
||
data = request.get_json(force=True, silent=True) or {}
|
||
cfg = load_config()
|
||
bg = cfg.setdefault("background", {})
|
||
for k in ("type", "color1", "color2", "animated", "imagePath"):
|
||
if k in data:
|
||
bg[k] = data[k]
|
||
save_config(cfg)
|
||
return jsonify(bg)
|
||
|
||
|
||
@api_bp.route("/api/feedback", methods=["PUT"])
|
||
@login_required
|
||
def api_set_feedback():
|
||
data = request.get_json(force=True, silent=True) or {}
|
||
cfg = load_config()
|
||
fb = cfg.setdefault("feedback", {})
|
||
for k in ("okColor", "errorColor"):
|
||
if k in data:
|
||
fb[k] = data[k]
|
||
for k in ("okWidth", "errorWidth", "glowRadius"):
|
||
if k in data:
|
||
try:
|
||
fb[k] = int(data[k])
|
||
except (TypeError, ValueError):
|
||
pass
|
||
save_config(cfg)
|
||
return jsonify(fb)
|
||
|
||
|
||
@api_bp.route("/api/settings", methods=["PUT"])
|
||
@login_required
|
||
def api_set_settings():
|
||
data = request.get_json(force=True, silent=True) or {}
|
||
cfg = load_config()
|
||
st = cfg.setdefault("settings", {})
|
||
for k in ("darkMode", "password", "kioskMode", "iconsDir", "webPort"):
|
||
if k in data:
|
||
val = data[k]
|
||
if k == "webPort":
|
||
try:
|
||
val = int(val)
|
||
except (TypeError, ValueError):
|
||
abort(400, "invalid web port")
|
||
if val < 1024 or val > 65535:
|
||
abort(400, "web port must be 1024–65535")
|
||
st[k] = val
|
||
save_config(cfg)
|
||
return jsonify({"ok": True})
|
||
|
||
|
||
# --- icons ---
|
||
@api_bp.route("/api/icons", methods=["GET"])
|
||
@login_required
|
||
def api_list_icons():
|
||
return jsonify(list_icons())
|
||
|
||
|
||
@api_bp.route("/api/icons", methods=["POST"])
|
||
@login_required
|
||
def api_upload_icon():
|
||
if "file" not in request.files:
|
||
abort(400, "file required")
|
||
fobj = request.files["file"]
|
||
fname = Path(fobj.filename).name
|
||
if not fname:
|
||
abort(400, "empty filename")
|
||
ext = Path(fname).suffix.lower()
|
||
if ext not in ALLOWED_ICON_EXT:
|
||
abort(400, f"extension {ext} not allowed")
|
||
target = icons_dir() / fname
|
||
fobj.save(str(target))
|
||
return jsonify({"ok": True, "name": fname})
|
||
|
||
|
||
@api_bp.route("/api/icons/<path:fname>", methods=["DELETE"])
|
||
@login_required
|
||
def api_delete_icon(fname):
|
||
p = icons_dir() / fname
|
||
try:
|
||
p.resolve().relative_to(icons_dir().resolve())
|
||
except ValueError:
|
||
abort(400)
|
||
if p.exists():
|
||
p.unlink()
|
||
return jsonify({"ok": True})
|