This commit is contained in:
Arseniy Romenskiy 2025-12-27 05:02:10 +03:00
commit 02f3a1b884
32 changed files with 5597 additions and 0 deletions

476
Start.py Normal file
View file

@ -0,0 +1,476 @@
#!/usr/bin/python3
import os
from flask import Flask, flash, request, redirect, url_for, send_from_directory, render_template, jsonify
from werkzeug.utils import secure_filename
from time import strftime, localtime, sleep #Для (Time)
import json
import hashlib
import logging
import asyncio
import bd_module
import db_module
from datetime import date
put = os.path.dirname(os.path.realpath(__file__)) + "/"#Путь- (part-1)
with open(f"{put}settings.json", "r") as read_file:
settings = json.load(read_file)
import logging
if settings["log_type"] == "INFO":
level=logging.INFO
elif settings["log_type"] == "DEBUG":
level=logging.DEBUG
elif settings["log_type"] == "ERROR":
level=logging.ERROR
else:
print('Тип лога указан не верно, авто "INFO"\nВареанты: "INFO", "DEBUG", ERROR')
level=logging.INFO
logging.basicConfig(filename=settings["log_file"], level=level)
def authentication(token):
uid = bd_module.check_user_short_token(token)
#return uid, gid
return uid
def mi_ip(request):
try:
return request.headers['x-forwarded-for']
except:
return request.remote_addr
UPLOAD_FOLDER = put + 'png/'
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
print(UPLOAD_FOLDER)
print("SAS")
if not os.path.isdir(UPLOAD_FOLDER):
os.mkdir(UPLOAD_FOLDER)
@app.route('/login/', methods=['GET', 'POST'])
def login():
logging.debug(f"request.headers - {request.headers}")
email = request.headers['email']
password = request.headers['password']
remember = request.headers['remember']
ps_info_name = request.headers['User-Agent']
uid ,user_name, active = db_module.check_user(email, password)
if uid == None:
logging.info(f"Авторизаии под пользователям {email} !!!ОТКАЗ!!!")
return "login not found", 412
elif active == 0:
logging.info(f"Авторизаии под пользователям {email} !!!ОТКЛЮЧИНА!!!")
return "Учётная запесь отключина!", 423
else:
if remember == "1":
short_token, live_token = bd_module.add_live_token(uid, ps_info_name, mi_ip(request), date.today(),30*24*60*60)
#short_token = bd_module.add_short_token(uid)
#live_token = bd_module.add_live_token(uid)
else:
short_token, live_token = bd_module.add_live_token(uid, ps_info_name, mi_ip(request), date.today(),12*60*60)
#short_token = bd_module.add_short_token(uid)
#live_token = None
print(short_token, " - " ,live_token)
logging.info(f"Авторизаии под пользователям {email} !УСПЕХ!")
return json.dumps({"short_token": short_token, "live_token": live_token, "user_id": uid, "user_name": user_name}, separators=(',', ':'))
@app.route('/ls/', methods=['GET', 'POST'])
def ls():
print("ls")
print(request.headers)
print(request.headers['short_token'])
uid = authentication(request.headers['short_token'])
print(uid)
if uid == None:
print("/ls/' - 403")
return "403", 426
return json.dumps({
"ls": [
{"id": 1, "name": "office-laptop", "status": "active", "ip_local": "10.0.0.2/32", "ip_server": "198.51.100.10:51820"},
{"id": 2, "name": "phone", "status": "disabled", "ip_local": "10.0.0.3/32", "ip_server": "198.51.100.10:51820"}
]
}, separators=(',', ':'))
#return json.dumps({"ls": bd_module.ls_item(uid)}, separators=(',', ':'))
@app.route('/item_add/', methods=['GET', 'POST'])
def item_add():
print("item_add")
uid = authentication(request.headers['short_token'])
if uid == None:
logging.debug(f"ls - Отказ по токену! IP - {mi_ip(request)}")
print("/ls/' - 403")
return "403", 426
print(request)
content = request.json
print(content)
item_name = content['item_name']
status = content['status']
user_id_1 = content['user_id_1']
user_id_2 = content['user_id_2']
comments = content['comments']
inventory_id = content['inventory_id']
id = bd_module.add_item(uid, user_id_1, user_id_2, item_name, status, None, comments, inventory_id)
if id != 0:
logging.info(f"Пользователь {uid} создал предмет {id}")
return json.dumps({"item_id": id}, separators=(',', ':'))
else:
logging.info(f"Пользователю {uid} отказано в создании предмета")
return "Отказ", 403
@app.route('/item_edit/', methods=['GET', 'POST'])
def item_edit():
print("item_edit")
uid = authentication(request.headers['short_token'])
if uid == None:
logging.debug(f"item_edit - Отказ по токену! IP - {mi_ip(request)}")
print("/item_edit/' - 403")
return "403", 426
content = request.json
item_id = content['item_id']
item_name = content['item_name']
status = content['status']
user_id_1 = content['user_id_1']
user_id_2 = content['user_id_2']
comments = content['comments']
inventory_id = content['inventory_id']
id = bd_module.item_edit(uid, item_id, user_id_1, user_id_2, item_name, status, None, comments, inventory_id)
if id == 1:
logging.info(f"Пользователь {uid} изменил предмета {item_id}")
return "OK", 200
else:
logging.info(f"Пользователю {uid} отказано в редактировании предмета {item_id}")
return "Отказ", 403
@app.route('/item_rm/', methods=['GET', 'POST'])
def item_rm():
print("item_rm")
uid = authentication(request.headers['short_token'])
if uid == None:
logging.debug(f"item_rm - Отказ по токену! IP - {mi_ip(request)}")
print("/item_rm/' - 403")
return "403", 426
content = request.json
item_id = content['item_id']
id = bd_module.item_rm(uid, item_id)
if id == 1:
if os.path.isfile(f"{put}png/item/{item_id}"):
os.remove(f"{put}png/item/{item_id}")
logging.info(f"Иконка предмета {item_id} удалена")
logging.info(f"Пользователь {uid} удолил предмета {item_id}")
return "OK", 200
else:
logging.info(f"Пользователю {uid} отказанно в удоление предмета {item_id}")
return "Отказ", 403
@app.route('/user_ls/', methods=['GET', 'POST'])
def user_ls():
print("user_ls")
print(request.headers['short_token'])
uid = authentication(request.headers['short_token'])
print(uid)
if uid == None:
logging.debug(f"user_ls - Отказ по токену! IP - {mi_ip(request)}")
print("/user_ls/' - 403")
return "403", 426
return json.dumps({"ls": bd_module.ls_user(uid)}, separators=(',', ':'))
@app.route('/user_info/', methods=['GET', 'POST'])
def user_info():
uid = authentication(request.headers['short_token'])
user_id = (request.headers['user_id'])
#uid = 455435
if uid == None:
logging.debug(f"user_info - Отказ по токену! IP - {mi_ip(request)}")
print("/user_info/' - 403")
return "403", 426
user_info = bd_module.user_info(uid, user_id)
print(user_info)
if user_info != None:
#########################
directory = app.config['UPLOAD_FOLDER']
# user_info
print(user_info) ################################################################################################################
student = {
"user_name" : user_info[0],
"email" : user_info[1],
"avatar" : user_info[2],
"active": user_info[3],
"group_id": user_info[4],
"permission": user_info[5],
}
b = json.dumps(student)
return b
else:
return "Отказ в доступе!", 403
@app.route('/renew/', methods=['GET', 'POST'])
def renew():
print("renew")
short_token, live_token = bd_module.update_short_token(request.headers['user_id'], request.headers['live_token'])
#short_token = bd_module.check_user_live_token(request.headers['live_token'])
if short_token == None:
logging.debug(f"renew - Отказ по токену! IP - {mi_ip(request)}")
print("/renew/' - 403")
return "Токен не верен", 426
return json.dumps({"short_token": short_token, "live_token": live_token}, separators=(',', ':'))
"""
@app.route('/exit/', methods=['GET', 'POST']) # ??? Что это ???
def exit():
print("exit")
uid = authentication(request.headers['short_token'])
if uid == None:
logging.debug(f"exit - Отказ по токену!")
print("/exit/' - 403")
return "403", 426
return json.dumps({"short_token": short_token}, separators=(',', ':'))
"""
@app.route('/kill_session/', methods=['GET', 'POST'])
def kill_session():
print("kill_session")
user_id = request.headers['user_id']
live_token = request.headers['live_token']
print(live_token)
A = bd_module.rm_live_token(user_id, live_token)
if A:
return "OK"
else:
return "404", 404
@app.route('/ls_sessions/', methods=['GET', 'POST'])
def ls_sessions(): # !!! МОГУТ БЫТЬ ПРОБЛЕМЫ !!!
uid = authentication(request.headers['short_token'])
if uid == None:
logging.debug(f"ls_sessions - Отказ по токену! IP - {mi_ip(request)}")
print("/ls_sessions/' - 426")
return "426", 426
A = bd_module.ls_sessions(uid)
return json.dumps({"matrix": A}, separators=(',', ':'))
@app.route('/exiting_session/', methods=['GET', 'POST'])
def exiting_session(): # !!! МОГУТ БЫТЬ ПРОБЛЕМЫ !!!
uid = authentication(request.headers['short_token'])
if uid == None:
logging.debug(f"exiting_session - Отказ по токену! IP - {mi_ip(request)}")
print("/exiting_session/' - 426")
return "426", 426
position = request.headers['position']
A = bd_module.rm_live_token_position(uid, position)
return "OK"
@app.route('/full_closure_session/', methods=['GET', 'POST'])
def full_closure_session(): # !!! МОГУТ БЫТЬ ПРОБЛЕМЫ !!!
uid = authentication(request.headers['short_token'])
if uid == None:
logging.debug(f"full_closure_session - Отказ по токену! IP - {mi_ip(request)}")
print("/full_closure_session/' - 426")
return "426", 426
A = bd_module.full_sessions_kill(uid)
return "OK"
@app.route('/user_add/', methods=['GET', 'POST'])
def user_add():
uid = authentication(request.headers['short_token'])
if uid == None:
logging.debug(f"user_add - Отказ по токену! IP - {mi_ip(request)}")
print("/user_add/' - 426")
return "426", 426
content = request.json
user_name = content['user_name']
email = content['email']
password = content['password']
avatar = content['avatar']
active = content['active']
group_id = content['group_id']
A = bd_module.user_add(uid, user_name, email, password, avatar, active, group_id)
if A == None:
return "Отказ в доступе!", 403
return json.dumps({"user_id": A}, separators=(',', ':'))
@app.route('/user_edit/', methods=['GET', 'POST'])
def user_edit():
uid = authentication(request.headers['short_token'])
if uid == None:
logging.debug(f"user_edit - Отказ по токену! IP - {mi_ip(request)}")
print("/user_edit/' - 426")
return "426", 426
content = request.json
user_id = content['user_id']
user_name = content['user_name']
email = content['email']
password = content['password']
avatar = content['avatar']
active = content['active']
group_id = content['group_id']
A = bd_module.user_edit(uid, user_id, user_name, email, password, avatar, active, group_id)
if A == 0:
return "Отказ в доступе!", 403
return "OK"
@app.route('/ls_group/', methods=['GET', 'POST'])
def ls_group():
uid = authentication(request.headers['short_token'])
if uid == None:
logging.debug(f"ls_group - Отказ по токену! IP - {mi_ip(request)}")
print("/ls_group/' - 426")
return "426", 426
A = bd_module.ls_group(uid)
return json.dumps({"matrix": A}, separators=(',', ':'))
"""
@app.route('/permission/', methods=['GET', 'POST'])
def permission():
print("permission")
print(request.headers['short_token'])
uid = authentication(request.headers['short_token'])
print(uid)
if uid == None:
print("/permission/' - 426")
return "426", 426
user_id = request.headers["user_id"]
A = permission(uid,user_id)
return json.dumps({"ls": bd_module.ls_user(uid)}, separators=(',', ':'))
"""
@app.route('/', methods=['GET', 'POST'])
def lol():
#return request.remote_addr
#return request.headers['x-forwarded-for']
#return render_template("frontend/index.html")
return mi_ip(request)
@app.route('/SAS/', methods=['GET', 'POST'])
def ASA():
content = request.json
print(content)
return "SAS"
@app.route('/rm_item_icon/', methods=['GET', 'POST'])
def rm_item_icon():
uid = authentication(request.headers['short_token'])
#uid = 1
item_id = request.headers['item_id']
if uid == None:
logging.debug(f"rm_item_icon - Отказ по токену! IP - {mi_ip(request)}")
print("/rm_item_icon/' - 403")
return "403", 426
a = bd_module.item_edit(uid, item_id, None, None, None, None, '-1', None, None)
if a == 1:
if os.path.isfile(f"{put}png/item/{item_id}"):
os.remove(f"{put}png/item/{item_id}")
return "OK"
return "Отказ в доступе!", 403
@app.route('/add_item_icon/', methods=['GET', 'POST'])
def add_item_icon():
uid = authentication(request.headers['short_token'])
#uid = 1
item_id = request.headers['item_id']
if uid == None:
logging.debug(f"add_item_icon - Отказ по токену! IP - {mi_ip(request)}")
print("/add_item_icon/' - 403")
return "403", 426
if 'file' in request.files:
if bd_module.item_w_test(uid,item_id) == 1:
file = request.files['file']
print(file)
# безопасно извлекаем оригинальное имя файла
filename = secure_filename(file.filename)
directory = app.config['UPLOAD_FOLDER'] + "item"
file.save(os.path.join(directory, item_id))
hash = hashlib.md5(open(f"{UPLOAD_FOLDER}item/{item_id}",'rb').read()).hexdigest()
bd_module.item_edit(uid, item_id, None, None, None, None, hash, None, None)
return f"{hash}"
return "Отказ в доступе!", 403
return "Файла нет"
"""
if request.method == 'POST':
file = request.files['file']
#if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return redirect(url_for('uploaded_file', filename=filename))
return "OF"
"""
@app.route('/ls_item_icon/', methods=['GET', 'POST'])
def ls_item_icon():
print("item_icon")
item_id = request.headers['item_id']
uid = authentication(request.headers['short_token'])
#uid = 1
print(uid)
if uid == None:
logging.debug(f"ls_item_icon - Отказ по токену! IP - {mi_ip(request)}")
print("/ls_item_icon/' - 403")
return "403", 426
item_info = bd_module.item_info(uid,item_id)
if item_info != None:
if item_info[5] != None:
directory = app.config['UPLOAD_FOLDER']
return send_from_directory(directory=directory+"item", path=str(item_id))
return "Нет изображения", 404
return "Нет такого предмета", 404
@app.route('/ls_avatar_icon/', methods=['GET', 'POST'])
def ls_avatar_icon():
uid = authentication(request.headers['short_token'])
url = request.headers['uid']
#uid = 455435
if uid == None:
logging.debug(f"avatar - Отказ по токену! IP - {mi_ip(request)}")
print("/avatar/' - 403")
return "403", 426
url = bd_module.avatar_png(uid)
if url != None:
if int(url) == uid:
directory = app.config['UPLOAD_FOLDER']
return send_from_directory(directory=directory+"user", path=uid)
### SAS
directory = app.config['UPLOAD_FOLDER']
return send_from_directory(directory=directory+"user", path=url)
return None
if __name__ == "__main__":
app.run(host=settings["host"], port=settings["port"])

Binary file not shown.

Binary file not shown.

1593
bd_module)_OLD.py Normal file

File diff suppressed because it is too large Load diff

748
bd_module.py Normal file
View file

@ -0,0 +1,748 @@
import os
import json
import random
import secrets
from time import strftime # Для (Time)
from datetime import timedelta
import asyncio
import aiosqlite
# BEGIN redis (оставлено синхронным как в исходнике)
import redis
put = os.path.dirname(os.path.realpath(__file__)) + "/"
with open(f"{put}settings.json", "r", encoding="utf-8") as read_file:
settings = json.load(read_file)
r = redis.StrictRedis(
host=settings["Redis"]["host"],
port=settings["Redis"]["port"],
password=settings["Redis"]["password"],
encoding="utf-8",
decode_responses=True,
db=settings["Redis"]["db"],
)
# END redis
# -------------------------
# SQLite (async) backend
# -------------------------
_SQLITE_PATH = (
settings.get("Sqlite", {}).get("path")
or os.path.join(put, "recording_spark.sqlite3")
)
# Если раньше в settings.json было Mariadb.select_commit — оставим аналог
_SQLITE_SELECT_COMMIT = bool(settings.get("Sqlite", {}).get("select_commit", False))
_db: aiosqlite.Connection | None = None
_db_init_lock = asyncio.Lock()
_db_initialized = False
async def _connect_db() -> aiosqlite.Connection:
"""
Возвращает глобальное соединение aiosqlite, инициализирует схему при первом обращении.
"""
global _db, _db_initialized
async with _db_init_lock:
if _db is None:
_db = await aiosqlite.connect(_SQLITE_PATH)
_db.row_factory = aiosqlite.Row
await _db.execute("PRAGMA foreign_keys = ON;")
await _db.execute("PRAGMA journal_mode = WAL;")
await _db.execute("PRAGMA synchronous = NORMAL;")
await _db.execute("PRAGMA busy_timeout = 5000;")
if not _db_initialized:
await _init_schema(_db)
_db_initialized = True
return _db
async def _init_schema(db: aiosqlite.Connection) -> None:
"""
Создаёт таблицы и индексы в SQLite.
Заменяет MySQL/MariaDB схему recording_spark.* на rs_* (без schema prefix).
"""
await db.executescript(
"""
CREATE TABLE IF NOT EXISTS rs_group (
group_id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
rights INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS rs_user (
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_name TEXT NOT NULL,
email TEXT,
password TEXT,
avatar INTEGER NOT NULL DEFAULT 0,
active INTEGER NOT NULL DEFAULT 0,
group_id INTEGER NOT NULL,
FOREIGN KEY (group_id) REFERENCES rs_group (group_id)
);
CREATE TABLE IF NOT EXISTS rs_item (
item_id INTEGER PRIMARY KEY AUTOINCREMENT,
item_name TEXT NOT NULL,
status TEXT,
user_id_1 INTEGER NOT NULL,
user_id_2 INTEGER NOT NULL,
icon TEXT DEFAULT NULL,
comments TEXT,
inventory_id INTEGER,
FOREIGN KEY (user_id_1) REFERENCES rs_user (user_id),
FOREIGN KEY (user_id_2) REFERENCES rs_user (user_id)
);
CREATE TABLE IF NOT EXISTS rs_ownership_over_group (
group_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
FOREIGN KEY (group_id) REFERENCES rs_group (group_id),
FOREIGN KEY (user_id) REFERENCES rs_user (user_id)
);
CREATE INDEX IF NOT EXISTS idx_rs_user_group_id ON rs_user(group_id);
CREATE INDEX IF NOT EXISTS idx_rs_item_user1 ON rs_item(user_id_1);
CREATE INDEX IF NOT EXISTS idx_rs_item_user2 ON rs_item(user_id_2);
CREATE INDEX IF NOT EXISTS idx_rs_oog_group_id ON rs_ownership_over_group(group_id);
CREATE INDEX IF NOT EXISTS idx_rs_oog_user_id ON rs_ownership_over_group(user_id);
"""
)
await db.commit()
async def close_db() -> None:
global _db, _db_initialized
if _db is not None:
await _db.close()
_db = None
_db_initialized = False
async def ping_db() -> None:
"""
В MariaDB это был ping/reconnect. В SQLite (aiosqlite) просто гарантируем, что соединение поднято.
"""
await _connect_db()
async def _fetchone(q: str, params: tuple = ()) -> aiosqlite.Row | None:
db = await _connect_db()
if _SQLITE_SELECT_COMMIT:
await db.commit()
async with db.execute(q, params) as cur:
return await cur.fetchone()
async def _fetchall(q: str, params: tuple = ()) -> list[aiosqlite.Row]:
db = await _connect_db()
if _SQLITE_SELECT_COMMIT:
await db.commit()
async with db.execute(q, params) as cur:
return await cur.fetchall()
async def _execute(q: str, params: tuple = (), *, commit: bool = False) -> aiosqlite.Cursor:
db = await _connect_db()
cur = await db.execute(q, params)
if commit:
await db.commit()
return cur
# -------------------------
# Rights / permissions
# -------------------------
# Биты прав как в исходнике (mariadb):
# item_r: bits 0,2,4
# item_w: bits 1,3,5
# user_r: bits 6,8,10
# user_w: bits 7,9,11
_BITS = {
"item_r": (0, 2, 4),
"item_w": (1, 3, 5),
"user_r": (6, 8, 10),
"user_w": (7, 9, 11),
}
async def _get_user_rights(user_id: int) -> int | None:
row = await _fetchone(
"""
SELECT g.rights
FROM rs_user u
JOIN rs_group g ON u.group_id = g.group_id
WHERE u.user_id = ?
""",
(int(user_id),),
)
return None if row is None else int(row["rights"])
def _rights_tuple(rights: int, kind: str) -> tuple[int, int, int]:
b0, b1, b2 = _BITS[kind]
return ((rights >> b0) & 1, (rights >> b1) & 1, (rights >> b2) & 1)
async def permissions(user_id: int, kind: int) -> tuple[int, int, int] | None:
"""
kind: 0=item_r, 1=item_w, 2=user_r, 3=user_w
Возвращает 3 флага (self, group, all) как в исходнике.
"""
rights = await _get_user_rights(int(user_id))
if rights is None:
return None
if kind == 0:
return _rights_tuple(rights, "item_r")
if kind == 1:
return _rights_tuple(rights, "item_w")
if kind == 2:
return _rights_tuple(rights, "user_r")
if kind == 3:
return _rights_tuple(rights, "user_w")
raise ValueError("Unknown kind")
async def permissions_item_r(u_id: int):
return await permissions(u_id, 0)
async def permissions_item_w(u_id: int):
return await permissions(u_id, 1)
async def permissions_user_r(u_id: int):
return await permissions(u_id, 2)
async def permissions_user_w(u_id: int):
return await permissions(u_id, 3)
async def _owned_group_ids(owner_user_id: int) -> list[int]:
rows = await _fetchall(
"SELECT group_id FROM rs_ownership_over_group WHERE user_id = ?",
(int(owner_user_id),),
)
return [int(r["group_id"]) for r in rows]
async def _owned_user_ids(owner_user_id: int) -> list[int]:
gids = await _owned_group_ids(owner_user_id)
if not gids:
return []
placeholders = ",".join(["?"] * len(gids))
rows = await _fetchall(
f"SELECT user_id FROM rs_user WHERE group_id IN ({placeholders})",
tuple(gids),
)
return [int(r["user_id"]) for r in rows]
# -------------------------
# Token / sessions (Redis) — оставлено как было
# -------------------------
def update_short_token(user_id, live_token):
TTL = r.pttl(f"recording_spark:{user_id}:{live_token}:PC-INFO") / 1000
if TTL > 31 * 60:
rm_short_token(user_id, live_token)
list_A = r.lrange(f"recording_spark:{user_id}:{live_token}:PC-INFO", 0, -1)
for key in r.scan_iter(f"recording_spark:{user_id}:{live_token}:*"):
r.delete(key)
short_token, live_token = add_live_token(
user_id, list_A[0], list_A[1], list_A[2], int(TTL)
)
return short_token, live_token
return None, None
def rm_live_token(user_id, live_token):
short_token = r.get(f"recording_spark:{user_id}:{live_token}:short_token")
r.delete(f"recording_spark:short_token:{short_token}")
A = False
for key in r.scan_iter(f"recording_spark:{user_id}:{live_token}:*"):
A = True
r.delete(key)
return A
def rm_short_token(user_id, live_token):
short_token = r.get(f"recording_spark:{user_id}:{live_token}:short_token")
r.delete(f"recording_spark:short_token:{short_token}")
r.delete(f"recording_spark:{user_id}:{live_token}:short_token")
def add_short_token(user_id, live_token):
short_token = secrets.token_urlsafe(32)
r.set(f"recording_spark:short_token:{short_token}", user_id, 30 * 60)
r.set(f"recording_spark:{user_id}:{live_token}:short_token", short_token, 30 * 60)
return short_token
def add_live_token(user_id, ps_info_name, ps_info_ip, ps_info_date, TTL):
live_token = secrets.token_urlsafe(128)
r.rpush(
f"recording_spark:{str(user_id)}:{live_token}:PC-INFO",
ps_info_name,
ps_info_ip,
str(ps_info_date),
)
r.expire(f"recording_spark:{user_id}:{live_token}:PC-INFO", timedelta(seconds=TTL))
short_token = add_short_token(user_id, live_token)
return short_token, live_token
def check_user_live_token(user_id, live_token):
user_id = r.get(f"recording_spark:{user_id}:{live_token}:PC-INFO")
if user_id is None:
return None
short_token = update_short_token(user_id, live_token)
return short_token
def check_user_short_token(short_token):
user_id = r.get(f"recording_spark:short_token:{short_token}")
return None if user_id is None else user_id
def ls_sessions(user_id):
A = 0
Q = []
for key in r.scan_iter(f"recording_spark:{user_id}:*:PC-INFO"):
short_token = r.lrange(key, 0, -1)
short_token.append(A)
Q.append(short_token)
A = A + 1
return Q
def rm_live_token_position(user_id, position):
A = 0
for key in r.scan_iter(f"recording_spark:{user_id}:*"):
if position == A:
short_token = r.get(f"recording_spark:{user_id}:{key}:short_token")
r.delete(f"recording_spark:short_token:{short_token}")
for key in r.scan_iter(f"recording_spark:{user_id}:{key}:*"):
r.delete(key)
A = A + 1
def full_sessions_kill(user_id):
for key in r.scan_iter(f"recording_spark:{user_id}:*:short_token"):
short_token = r.get(key)
r.delete(f"recording_spark:short_token:{short_token}")
r.delete(key)
for key in r.scan_iter(f"recording_spark:{user_id}:*"):
r.delete(key)
def add_user_registration(email, password):
G = int(strftime("%Y"))
M = int(strftime("%m"))
D = int(strftime("%d"))
ch = int(strftime("%H"))
m = int(strftime("%M"))
s = int(strftime("%S"))
id = f"{D}{ch}{m}{s}{random.randint(0, 100000)}"
date = f"{G}-{M}-{D}"
r.set(f"recording_spark:registration:{id}:email", email, 7 * 24 * 60 * 60)
r.set(f"recording_spark:registration:{id}:password", password, 7 * 24 * 60 * 60)
r.set(f"recording_spark:registration:{id}:date", date, 7 * 24 * 60 * 60)
return id
# -------------------------
# DB functions (async) — аналог прежних, но на SQLite
# -------------------------
async def user_name(user_id: int):
await ping_db()
row = await _fetchone("SELECT user_name FROM rs_user WHERE user_id = ?", (int(user_id),))
return None if row is None else row["user_name"]
async def avatar_png(user_id: int):
await ping_db()
row = await _fetchone("SELECT avatar FROM rs_user WHERE user_id = ?", (int(user_id),))
return None if row is None else row["avatar"]
async def check_user(email: str, password: str):
await ping_db()
row = await _fetchone(
"SELECT user_id, user_name, active FROM rs_user WHERE email = ? AND password = ?",
(email, password),
)
if row is None:
return None, None, None
return int(row["user_id"]), row["user_name"], int(row["active"])
async def ls_user(user_id: int):
await ping_db()
A = await permissions_user_r(user_id)
if A is None:
return None
q = """
SELECT u.user_id, u.user_name, u.email, u.avatar, u.active, g.group_id, g.name, g.rights
FROM rs_user u
JOIN rs_group g ON u.group_id = g.group_id
"""
params: list = []
if A[2] == 1:
rows = await _fetchall(q, ())
return [list(r) for r in rows]
conds = []
if A[0] == 1:
conds.append("u.user_id = ?")
params.append(int(user_id))
if A[1] == 1:
conds.append(
"u.user_id IN (SELECT user_id FROM rs_user WHERE group_id IN (SELECT group_id FROM rs_ownership_over_group WHERE user_id = ?))"
)
params.append(int(user_id))
if not conds:
return []
q += " WHERE " + " OR ".join(conds)
rows = await _fetchall(q, tuple(params))
return [list(r) for r in rows]
async def ls_item(user_id: int):
await ping_db()
A = await permissions_item_r(user_id)
if A is None:
return None
q = """
SELECT i.item_id, i.item_name, i.status,
i.user_id_1, u1.user_name,
i.user_id_2, u2.user_name,
i.icon, i.comments, i.inventory_id
FROM rs_item i
JOIN rs_user u1 ON i.user_id_1 = u1.user_id
JOIN rs_user u2 ON i.user_id_2 = u2.user_id
"""
params: list = []
if A[2] == 1:
rows = await _fetchall(q, ())
return [list(r) for r in rows]
conds = []
if A[0] == 1:
conds.append("(i.user_id_1 = ? OR i.user_id_2 = ?)")
params.extend([int(user_id), int(user_id)])
if A[1] == 1:
conds.append(
"i.user_id_2 IN (SELECT user_id FROM rs_user WHERE group_id IN (SELECT group_id FROM rs_ownership_over_group WHERE user_id = ?))"
)
params.append(int(user_id))
if not conds:
return []
q += " WHERE " + " OR ".join(conds)
rows = await _fetchall(q, tuple(params))
return [list(r) for r in rows]
async def user_add(user_id: int, user_name: str, email: str, password: str, avatar: int, active: int, group_id: int):
await ping_db()
A = await permissions_user_w(user_id)
if A is None:
return None
if A[2] != 1:
if A[1] == 1:
owned = await _owned_group_ids(user_id)
if int(group_id) not in owned:
return None
else:
return None
cur = await _execute(
"""
INSERT INTO rs_user (user_name, email, password, avatar, active, group_id)
VALUES (?, ?, ?, ?, ?, ?)
""",
(user_name, email, password, int(avatar), int(active), int(group_id)),
commit=True,
)
return int(cur.lastrowid)
async def _can_read_user(viewer_id: int, target_user_id: int) -> bool:
A = await permissions_user_r(viewer_id)
if A is None:
return False
if A[2] == 1:
return True
if A[0] == 1 and int(viewer_id) == int(target_user_id):
return True
if A[1] == 1:
owned_users = await _owned_user_ids(viewer_id)
return int(target_user_id) in owned_users
return False
async def _can_write_user(actor_id: int, target_user_id: int) -> bool:
A = await permissions_user_w(actor_id)
if A is None:
return False
if A[2] == 1:
return True
if A[0] == 1 and int(actor_id) == int(target_user_id):
return True
if A[1] == 1:
owned_users = await _owned_user_ids(actor_id)
return int(target_user_id) in owned_users
return False
async def user_edit(user_id: int, user_id_rec: int, user_name, email, password, avatar, active, group_id):
await ping_db()
if not await _can_write_user(user_id, user_id_rec):
return 0
if group_id is not None:
A = await permissions_user_w(user_id)
if A is None:
return 0
if A[2] != 1:
owned = await _owned_group_ids(user_id)
if int(group_id) not in owned:
return 0
cur = await _execute(
"""
UPDATE rs_user
SET
user_name = COALESCE(?, user_name),
email = COALESCE(?, email),
password = COALESCE(?, password),
avatar = COALESCE(?, avatar),
active = COALESCE(?, active),
group_id = COALESCE(?, group_id)
WHERE user_id = ?
""",
(user_name, email, password, avatar, active, group_id, int(user_id_rec)),
commit=True,
)
changed = int(cur.rowcount)
if changed == 1 and (active == 0 or password is not None):
full_sessions_kill(user_id_rec)
return changed
async def user_info(user_id: int, user_id_rec: int):
await ping_db()
if not await _can_read_user(user_id, user_id_rec):
return None
row = await _fetchone(
"""
SELECT u.user_name, u.email, u.avatar, u.active, g.group_id, g.rights
FROM rs_user u
JOIN rs_group g ON u.group_id = g.group_id
WHERE u.user_id = ?
""",
(int(user_id_rec),),
)
return None if row is None else tuple(row)
async def ls_group(user_id: int):
await ping_db()
A = await permissions_user_r(user_id)
if A is None:
return None
if A[2] == 1:
rows = await _fetchall("SELECT group_id, name, rights FROM rs_group", ())
return [tuple(r) for r in rows]
rows = await _fetchall(
"""
SELECT group_id, name, rights
FROM rs_group
WHERE group_id IN (SELECT group_id FROM rs_ownership_over_group WHERE user_id = ?)
""",
(int(user_id),),
)
return [tuple(r) for r in rows]
async def _can_read_item(viewer_id: int, item_row: aiosqlite.Row) -> bool:
A = await permissions_item_r(viewer_id)
if A is None:
return False
if A[2] == 1:
return True
if A[0] == 1 and (int(item_row["user_id_1"]) == int(viewer_id) or int(item_row["user_id_2"]) == int(viewer_id)):
return True
if A[1] == 1:
owned_users = await _owned_user_ids(viewer_id)
return int(item_row["user_id_2"]) in owned_users
return False
async def _can_write_item(actor_id: int, item_row: aiosqlite.Row) -> bool:
A = await permissions_item_w(actor_id)
if A is None:
return False
if A[2] == 1:
return True
if A[0] == 1 and int(item_row["user_id_1"]) == int(actor_id):
return True
if A[1] == 1:
owned_users = await _owned_user_ids(actor_id)
return int(item_row["user_id_1"]) in owned_users
return False
async def item_info(user_id: int, item_id: int):
await ping_db()
row = await _fetchone("SELECT * FROM rs_item WHERE item_id = ?", (int(item_id),))
if row is None:
return None
if not await _can_read_item(user_id, row):
return None
return tuple(row)
async def item_w_test(user_id: int, item_id: int):
await ping_db()
row = await _fetchone("SELECT * FROM rs_item WHERE item_id = ?", (int(item_id),))
if row is None:
return 0
return 1 if await _can_write_item(user_id, row) else 0
async def item_edit(user_id: int, item_id: int, user_id_1, user_id_2, name, status, icon, comments, inventory_id):
await ping_db()
row = await _fetchone("SELECT * FROM rs_item WHERE item_id = ?", (int(item_id),))
if row is None:
return 0
if not await _can_write_item(user_id, row):
return 0
if user_id_1 is not None and int(user_id_1) != -1:
if not await _can_read_user(user_id, int(user_id_1)) and int(user_id) != int(user_id_1):
return 0
if user_id_2 is not None and int(user_id_2) != -1:
if not await _can_read_user(user_id, int(user_id_2)) and int(user_id) != int(user_id_2):
return 0
icon_val = icon
if icon is not None and str(icon) == "-1":
icon_val = None
inv_val = inventory_id
if inventory_id is not None and int(inventory_id) == -1:
inv_val = None
cur = await _execute(
"""
UPDATE rs_item
SET
item_name = COALESCE(?, item_name),
status = COALESCE(?, status),
user_id_1 = COALESCE(?, user_id_1),
user_id_2 = COALESCE(?, user_id_2),
icon = COALESCE(?, icon),
comments = COALESCE(?, comments),
inventory_id = COALESCE(?, inventory_id)
WHERE item_id = ?
""",
(name, status, user_id_1, user_id_2, icon_val, comments, inv_val, int(item_id)),
commit=True,
)
return int(cur.rowcount)
async def item_rm(user_id: int, item_id: int):
await ping_db()
row = await _fetchone("SELECT * FROM rs_item WHERE item_id = ?", (int(item_id),))
if row is None:
return 0
if not await _can_write_item(user_id, row):
return 0
cur = await _execute("DELETE FROM rs_item WHERE item_id = ?", (int(item_id),), commit=True)
return int(cur.rowcount)
async def add_item(user_id: int, user_id_1: int, user_id_2: int, name: str, status, icon, comments, inventory_id):
await ping_db()
A = await permissions_item_w(user_id)
if A is None:
return 0
if A[2] != 1:
ok = False
if A[0] == 1 and (int(user_id_1) == int(user_id) or int(user_id_2) == int(user_id)):
ok = True
if not ok and A[1] == 1:
owned_users = await _owned_user_ids(user_id)
if int(user_id_1) == int(user_id) or int(user_id_2) == int(user_id):
ok = True
if int(user_id_1) in owned_users or int(user_id_2) in owned_users:
ok = True
if not ok:
return 0
cur = await _execute(
"""
INSERT INTO rs_item (item_name, status, user_id_1, user_id_2, icon, comments, inventory_id)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(name, status, int(user_id_1), int(user_id_2), icon, comments, inventory_id),
commit=True,
)
return int(cur.lastrowid)
async def user_rm(user_id: int, user_id_rm: int):
await ping_db()
if int(user_id) == int(user_id_rm):
return 0
if not await _can_write_user(user_id, user_id_rm):
return 0
db = await _connect_db()
try:
await db.execute("BEGIN;")
await db.execute(
"DELETE FROM rs_ownership_over_group WHERE user_id = ?",
(int(user_id_rm),),
)
cur2 = await db.execute(
"DELETE FROM rs_user WHERE user_id = ?",
(int(user_id_rm),),
)
await db.commit()
return int(cur2.rowcount)
except Exception:
await db.rollback()
raise
# Константы (оставлены, если где-то используются извне)
admin = 0
meneger = 34815
sender = 41935
user = 409665

BIN
data.sqlite3 Normal file

Binary file not shown.

32
db_module.py Normal file
View file

@ -0,0 +1,32 @@
import sqlite3
import os
put = os.path.dirname(os.path.realpath(__file__)) + "/" #Путь- (part-1)
DB_PATH = put + "data.sqlite3"
bd = sqlite3.connect(DB_PATH)
sql = bd.cursor()
sql.execute("""CREATE TABLE IF NOT EXISTS rs_user (
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_name TEXT NOT NULL,
email TEXT,
password TEXT,
avatar INTEGER NOT NULL DEFAULT 0,
active INTEGER NOT NULL DEFAULT 0
);""")
sql.close()
def check_user(email: str, password: str):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
try:
row = conn.execute(
"SELECT user_id, user_name, active FROM rs_user WHERE email = ? AND password = ?",
(email, password),
).fetchone()
if row is None:
return None, None, None
return int(row["user_id"]), row["user_name"], int(row["active"])
finally:
conn.close()

60
frontend/app.html Executable file
View file

@ -0,0 +1,60 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Панель</title>
<link rel="stylesheet" href="./assets/styles.css" />
</head>
<body>
<header class="header">
<div class="header-inner">
<div class="brand">WireGuard</div>
<div class="header-right">
<div class="userbox">
<div class="userlabel">Пользователь</div>
<div id="userName" class="username"></div>
</div>
<button id="logoutBtn" class="btn btn-ghost btn-sm">Выход</button>
</div>
</div>
</header>
<main class="container">
<section class="card">
<div class="table-head">
<div>
<h1 class="title">Конфиги</h1>
<div id="status" class="muted"></div>
</div>
<div class="table-controls">
<input id="search" class="input" placeholder="Поиск…" />
<button id="reloadBtn" class="btn btn-primary btn-sm">Обновить</button>
</div>
</div>
<div class="table-wrap">
<table class="table" id="dataTable">
<thead>
<tr>
<th>ID</th>
<th>Имя</th>
<th>Статус</th>
<th>IP локальный</th>
<th>IP сервер</th>
<th>Скачать</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</div>
<div id="errorBox" class="errorbox hidden"></div>
</section>
</main>
<script type="module" src="./js/app.js"></script>
</body>
</html>

207
frontend/assets/styles.css Executable file
View file

@ -0,0 +1,207 @@
:root{
--bg:#0b1020;
--card:#111a33;
--card2:#0f1730;
--text:#e7ecff;
--muted:#aab4da;
--line:#243055;
--shadow: 0 16px 40px rgba(0,0,0,.35);
--radius: 18px;
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial;
background: radial-gradient(1200px 800px at 20% 20%, #18224a 0%, var(--bg) 55%, #070b17 100%);
color:var(--text);
}
a{color:inherit}
.link{color:var(--muted); text-decoration:none}
.link:hover{color:var(--text); text-decoration:underline}
.center{
min-height:100%;
display:grid;
place-items:center;
padding:24px;
}
.container{max-width:1100px; margin:0 auto; padding:24px}
.card{
width:min(920px, 100%);
background: linear-gradient(180deg, var(--card), var(--card2));
border: 1px solid rgba(255,255,255,.06);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 22px;
}
.hero{width:min(720px, 100%); padding:32px}
.auth{width:min(520px, 100%)}
.topline{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:10px;
}
.title{margin:0 0 10px 0; font-size:32px; letter-spacing:.2px}
.muted{color:var(--muted)}
.actions{
display:grid;
grid-template-columns:1fr 1fr;
gap:14px;
margin-top:18px;
}
@media (max-width:640px){
.actions{grid-template-columns:1fr}
}
.form{display:grid; gap:12px; margin-top:10px}
.field{display:grid; gap:6px}
.label{color:var(--muted); font-size:14px}
input{
width:100%;
padding:12px 12px;
border-radius:12px;
border:1px solid rgba(255,255,255,.10);
background: rgba(0,0,0,.18);
color:var(--text);
outline:none;
}
input:focus{border-color: rgba(255,255,255,.25)}
.check{display:flex; gap:10px; align-items:center; color:var(--muted); font-size:14px}
.msg{min-height:22px; color:var(--muted)}
.btn{
appearance:none;
border:1px solid rgba(255,255,255,.12);
background: rgba(255,255,255,.06);
color:var(--text);
border-radius: 14px;
padding: 12px 14px;
cursor:pointer;
text-decoration:none;
display:inline-flex;
align-items:center;
justify-content:center;
gap:10px;
user-select:none;
}
.btn:hover{border-color: rgba(255,255,255,.22)}
.btn:active{transform: translateY(1px)}
.btn-primary{
background: linear-gradient(180deg, rgba(120,140,255,.28), rgba(90,110,255,.16));
border-color: rgba(120,140,255,.40);
}
.btn-ghost{background: rgba(255,255,255,.04)}
.btn-xl{padding:18px 16px; font-size:18px; border-radius:16px}
.btn-lg{padding:14px 14px; font-size:16px}
.btn-sm{padding:10px 12px; font-size:14px; border-radius:12px}
.header{
position:sticky;
top:0;
z-index:5;
backdrop-filter: blur(10px);
background: rgba(6,10,22,.55);
border-bottom:1px solid rgba(255,255,255,.06);
}
.header-inner{
max-width:1100px;
margin:0 auto;
padding:14px 24px;
display:flex;
align-items:center;
justify-content:space-between;
gap:16px;
}
.brand{font-weight:700; letter-spacing:.3px}
.header-right{display:flex; align-items:center; gap:14px}
.userbox{display:grid; gap:2px}
.userlabel{font-size:12px; color:var(--muted)}
.username{font-weight:600}
.table-head{
display:flex;
align-items:flex-end;
justify-content:space-between;
gap:12px;
margin-bottom:12px;
}
.table-controls{display:flex; align-items:center; gap:10px}
.input{min-width:240px}
@media (max-width:760px){
.table-head{flex-direction:column; align-items:stretch}
.table-controls{justify-content:space-between}
.input{min-width:0; flex:1}
}
.table-wrap{
overflow:auto;
border:1px solid rgba(255,255,255,.06);
border-radius:14px;
}
.table{
width:100%;
border-collapse:collapse;
min-width:980px;
}
th,td{
padding:12px 12px;
border-bottom:1px solid rgba(255,255,255,.06);
vertical-align:top;
font-size:14px;
}
th{
position:sticky;
top:0;
background: rgba(12,18,38,.9);
text-align:left;
color:var(--muted);
font-weight:600;
}
tr:hover td{background: rgba(255,255,255,.03)}
.badge{
display:inline-flex;
padding:6px 10px;
border-radius:999px;
border:1px solid rgba(255,255,255,.12);
color:var(--muted);
background: rgba(255,255,255,.04);
}
.pre{
margin:10px 0 0;
padding:12px;
border-radius:12px;
background: rgba(0,0,0,.18);
border:1px solid rgba(255,255,255,.08);
overflow:auto;
}
.callout{
margin-top:14px;
padding:14px;
border-radius:14px;
border:1px solid rgba(255,255,255,.10);
background: rgba(255,255,255,.04);
}
.callout-title{font-weight:600; color:var(--text)}
.errorbox{
margin-top:12px;
padding:12px;
border-radius:14px;
border:1px solid rgba(255,80,80,.35);
background: rgba(255,80,80,.08);
color: var(--text);
}
.hidden{display:none}

24
frontend/index.html Executable file
View file

@ -0,0 +1,24 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Главная</title>
<link rel="stylesheet" href="./assets/styles.css" />
</head>
<body>
<main class="center">
<section class="card hero">
<h1 class="title">Я крутой</h1>
<p class="muted">Панель WireGuard</p>
<div class="actions">
<a class="btn btn-ghost btn-xl" href="./request.html">Запросить учётную запись</a>
<a class="btn btn-primary btn-xl" href="./login.html">Вход</a>
</div>
</section>
</main>
<script type="module" src="./js/index.js"></script>
</body>
</html>

97
frontend/js/api.js Executable file
View file

@ -0,0 +1,97 @@
import { CONFIG } from "./config.js";
import { loadSession, saveSession, clearSession } from "./storage.js";
function urlJoin(base, path) {
if (!base) return path;
return base.replace(/\/+$/, "") + "/" + path.replace(/^\/+/, "");
}
async function parseResponse(res) {
const ct = (res.headers.get("content-type") || "").toLowerCase();
if (ct.includes("application/json")) return await res.json();
const text = await res.text();
// иногда сервер шлёт json как text/plain
try { return JSON.parse(text); } catch { return text; }
}
export async function renewTokens() {
const s = loadSession();
if (!s) throw new Error("NO_SESSION");
// /renew/ ждёт user_id и live_token в headers :contentReference[oaicite:1]{index=1}
const res = await fetch(urlJoin(CONFIG.API_BASE_URL, CONFIG.ENDPOINTS.renew), {
method: "GET",
headers: {
"user_id": String(s.user_id),
"live_token": String(s.live_token)
}
});
if (res.status === CONFIG.AUTH_ERROR_STATUS) {
clearSession();
throw new Error("RENEW_DENIED");
}
if (!res.ok) {
const body = await parseResponse(res);
throw new Error(typeof body === "string" ? body : "RENEW_FAILED");
}
const data = await parseResponse(res);
if (!data?.short_token || !data?.live_token) {
throw new Error("BAD_RENEW_RESPONSE");
}
const next = { ...s, short_token: data.short_token, live_token: data.live_token };
saveSession(next);
return next;
}
export async function apiFetch(path, {
method = "GET",
headers = {},
body = null,
auth = true
} = {}) {
const s = loadSession();
const reqHeaders = { ...headers };
if (auth) {
if (!s?.short_token) throw new Error("NO_AUTH");
// Все защищённые ручки проверяют short_token в headers :contentReference[oaicite:2]{index=2}
reqHeaders["short_token"] = String(s.short_token);
}
const doRequest = async () => {
const res = await fetch(urlJoin(CONFIG.API_BASE_URL, path), {
method,
headers: {
...reqHeaders,
...(body ? { "content-type": "application/json" } : {})
},
body: body ? JSON.stringify(body) : null
});
return res;
};
let res = await doRequest();
// Авто-обновление токенов при 426 :contentReference[oaicite:3]{index=3}
if (auth && res.status === CONFIG.AUTH_ERROR_STATUS) {
await renewTokens();
// обновим заголовок short_token и повторим один раз
const s2 = loadSession();
reqHeaders["short_token"] = String(s2.short_token);
res = await doRequest();
}
const payload = await parseResponse(res);
if (!res.ok) {
const err = new Error("API_ERROR");
err.status = res.status;
err.payload = payload;
throw err;
}
return payload;
}

207
frontend/js/app.js Executable file
View file

@ -0,0 +1,207 @@
import { requireAuthOrRedirect, logout } from "./auth.js";
import { apiFetch } from "./api.js";
import { CONFIG } from "./config.js";
import { loadSession, saveSession, clearSession } from "./storage.js";
if (!requireAuthOrRedirect()) {
// редирект уже сделан
}
const userNameEl = document.getElementById("userName");
const logoutBtn = document.getElementById("logoutBtn");
const reloadBtn = document.getElementById("reloadBtn");
const statusEl = document.getElementById("status");
const tbody = document.getElementById("tbody");
const errorBox = document.getElementById("errorBox");
const searchEl = document.getElementById("search");
let rows = [];
function showError(text) {
errorBox.textContent = text;
errorBox.classList.remove("hidden");
}
function clearError() {
errorBox.textContent = "";
errorBox.classList.add("hidden");
}
function setStatus(text) {
statusEl.textContent = text;
}
function normalizeCell(v) {
if (v === null || v === undefined) return "";
return String(v);
}
function isWireguardLike(text) {
const t = String(text || "");
return t.includes("[Interface]") || t.includes("[Peer]") || t.includes("PrivateKey") || t.includes("AllowedIPs");
}
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
} catch {
// fallback
const ta = document.createElement("textarea");
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
ta.remove();
}
}
function renderTable(filter = "") {
const q = filter.trim().toLowerCase();
const filtered = !q ? rows : rows.filter(r => r.searchBlob.includes(q));
tbody.innerHTML = "";
for (const r of filtered) {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${escapeHtml(r.id)}</td>
<td>${escapeHtml(r.name)}</td>
<td>${escapeHtml(r.status)}</td>
<td>${escapeHtml(r.ipLocal)}</td>
<td>${escapeHtml(r.ipServer)}</td>
<td></td>
`;
const tdBtn = tr.lastElementChild;
const btn = document.createElement("button");
btn.className = "btn btn-ghost btn-sm";
btn.textContent = "Скачать";
btn.addEventListener("click", async () => {
clearError();
try {
await triggerDownloadById(r.id);
} catch (err) {
if (err?.status === CONFIG.AUTH_ERROR_STATUS) {
await logout();
location.replace("./login.html");
return;
}
showError("Не удалось скачать конфиг.");
setStatus("");
}
});
tdBtn.appendChild(btn);
tbody.appendChild(tr);
}
}
function escapeHtml(s) {
return String(s)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
async function loadData() {
clearError();
setStatus("Загрузка…");
try {
const data = await apiFetch(CONFIG.ENDPOINTS.list, { method: "GET", auth: true });
const list = Array.isArray(data?.ls) ? data.ls : Array.isArray(data) ? data : [];
rows = list
.map(mapLsItem)
.filter(Boolean)
.map(r => ({
...r,
searchBlob: [r.id, r.name, r.status, r.ipLocal, r.ipServer].join(" ").toLowerCase()
}));
renderTable(searchEl.value);
setStatus(`Готово. Строк: ${rows.length}`);
} catch (err) {
if (err?.status === CONFIG.AUTH_ERROR_STATUS) {
await logout();
location.replace("./login.html");
return;
}
showError("Ошибка загрузки данных.");
setStatus("");
}
}
(function init() {
const s = loadSession();
userNameEl.textContent = s?.user_name || `#${s?.user_id || "—"}`;
logoutBtn.addEventListener("click", async () => {
await logout();
location.replace("./index.html");
});
reloadBtn.addEventListener("click", loadData);
searchEl.addEventListener("input", () => renderTable(searchEl.value));
loadData();
})();
function filenameFromDisposition(cd) {
if (!cd) return null;
// filename*=UTF-8''...
let m = cd.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
if (m?.[1]) return decodeURIComponent(m[1].replace(/"/g, "").trim());
// filename="..."
m = cd.match(/filename\s*=\s*"?([^";]+)"?/i);
if (m?.[1]) return m[1].trim();
return null;
}
export async function apiFetchBlob(path, { method = "GET", headers = {}, auth = true } = {}) {
const s = loadSession();
const reqHeaders = { ...headers };
if (auth) {
if (!s?.short_token) throw new Error("NO_AUTH");
reqHeaders["short_token"] = String(s.short_token);
reqHeaders["Authorization"] = `Bearer ${t}`;
}
const doRequest = async () => {
return await fetch(path.startsWith("http") ? path : (CONFIG.API_BASE_URL ? CONFIG.API_BASE_URL.replace(/\/+$/, "") + path : path), {
method,
headers: reqHeaders
});
};
let res = await doRequest();
if (auth && res.status === CONFIG.AUTH_ERROR_STATUS) {
// renew + retry один раз
await (await import("./api.js")).renewTokens?.(); // на случай циклического импорта (если будет) — безопасно
const s2 = loadSession();
reqHeaders["short_token"] = String(s2.short_token);
res = await doRequest();
}
if (!res.ok) {
const text = await res.text().catch(() => "");
const err = new Error("API_BLOB_ERROR");
err.status = res.status;
err.payload = text;
throw err;
}
const blob = await res.blob();
const filename = filenameFromDisposition(res.headers.get("content-disposition"));
return { blob, filename };
}

65
frontend/js/auth.js Executable file
View file

@ -0,0 +1,65 @@
import { CONFIG } from "./config.js";
import { saveSession, loadSession, clearSession } from "./storage.js";
import { apiFetch } from "./api.js";
function deviceLabel() {
const ua = navigator.userAgent || "browser";
return ua.slice(0, 120);
}
export function isLoggedIn() {
return !!loadSession();
}
export function requireAuthOrRedirect() {
if (!isLoggedIn()) {
location.replace("./login.html");
return false;
}
return true;
}
export async function login(email, password, remember) {
const headers = {
email: String(email),
password: String(password),
remember: remember ? "1" : "0"
};
const data = await apiFetch(CONFIG.ENDPOINTS.login, { method: "GET", headers, auth: false });
if (!data?.short_token || !data?.live_token || !data?.user_id) {
throw new Error("BAD_LOGIN_RESPONSE");
}
saveSession({
short_token: data.short_token,
live_token: data.live_token,
user_id: data.user_id,
user_name: data.user_name || ""
});
return data;
}
export async function logout() {
const s = loadSession();
try {
if (s?.user_id && s?.live_token) {
// /kill_session/ удаляет live_token :contentReference[oaicite:5]{index=5}
await apiFetch(CONFIG.ENDPOINTS.logout, {
method: "GET",
headers: {
user_id: String(s.user_id),
live_token: String(s.live_token)
},
auth: false
});
}
} catch {
// игнор: локально всё равно очищаем
} finally {
clearSession();
}
}

16
frontend/js/config.js Executable file
View file

@ -0,0 +1,16 @@
export const CONFIG = {
// Если фронт и API на одном домене — оставь пусто: ""
// Если API отдельно — например: "https://api.example.com"
API_BASE_URL: "",
ENDPOINTS: {
login: "/api/login/",
renew: "/api/renew/",
list: "/api/ls/",
logout:"/api/kill_session/",
download_conf_prefix: "/api/download_conf/"
},
// В твоём сервере токен-ошибка возвращается как 426 :contentReference[oaicite:0]{index=0}
AUTH_ERROR_STATUS: 426
};

32
frontend/js/login.js Executable file
View file

@ -0,0 +1,32 @@
import { login, isLoggedIn } from "./auth.js";
if (isLoggedIn()) {
location.replace("./app.html");
}
const form = document.getElementById("loginForm");
const msg = document.getElementById("msg");
function setMsg(text) {
msg.textContent = text || "";
}
form.addEventListener("submit", async (e) => {
e.preventDefault();
setMsg("Вход…");
const email = document.getElementById("email").value.trim();
const password = document.getElementById("password").value;
const remember = document.getElementById("remember").checked;
try {
await login(email, password, remember);
location.replace("./app.html");
} catch (err) {
// Сервер: 412 (не найден логин), 423 (отключён), 426 (токен) :contentReference[oaicite:6]{index=6}
const status = err?.status;
if (status === 412) return setMsg("Логин/пароль неверны.");
if (status === 423) return setMsg("Учётная запись отключена.");
setMsg("Ошибка входа.");
}
});

21
frontend/js/storage.js Executable file
View file

@ -0,0 +1,21 @@
const KEY = "wg_front_session_v1";
export function loadSession() {
try {
const raw = localStorage.getItem(KEY);
if (!raw) return null;
const s = JSON.parse(raw);
if (!s?.short_token || !s?.live_token || !s?.user_id) return null;
return s;
} catch {
return null;
}
}
export function saveSession(session) {
localStorage.setItem(KEY, JSON.stringify(session));
}
export function clearSession() {
localStorage.removeItem(KEY);
}

43
frontend/login.html Executable file
View file

@ -0,0 +1,43 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Вход</title>
<link rel="stylesheet" href="./assets/styles.css" />
</head>
<body>
<main class="center">
<section class="card auth">
<div class="topline">
<a class="link" href="./index.html">На главную</a>
</div>
<h1 class="title">Вход</h1>
<form id="loginForm" class="form">
<label class="field">
<span class="label">Логин (email)</span>
<input id="email" name="email" type="text" autocomplete="username" required />
</label>
<label class="field">
<span class="label">Пароль</span>
<input id="password" name="password" type="password" autocomplete="current-password" required />
</label>
<label class="check">
<input id="remember" type="checkbox" />
<span>Запомнить (длинная сессия)</span>
</label>
<button class="btn btn-primary btn-lg" type="submit">Войти</button>
<div id="msg" class="msg" role="status" aria-live="polite"></div>
</form>
</section>
</main>
<script type="module" src="./js/login.js"></script>
</body>
</html>

33
frontend/request.html Executable file
View file

@ -0,0 +1,33 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Запрос учётной записи</title>
<link rel="stylesheet" href="./assets/styles.css" />
</head>
<body>
<main class="center">
<section class="card">
<div class="topline">
<a class="link" href="./index.html">На главную</a>
<a class="link" href="./login.html">Вход →</a>
</div>
<h1 class="title">Запрос учётной записи</h1>
<p class="muted">
Тут обычно: кому писать/что отправить. Если у тебя есть API-ручка под регистрацию — подключишь в `js/api.js`.
</p>
<div class="callout">
<div class="callout-title">Шаблон заявки</div>
<pre class="pre">Нужен доступ к WireGuard.
ФИО:
Контакт:
Причина:
Устройство/ОС:</pre>
</div>
</section>
</main>
</body>
</html>

1153
server.log Normal file

File diff suppressed because it is too large Load diff

20
settings.json Normal file
View file

@ -0,0 +1,20 @@
{
"Mariadb": {
"host": "127.0.0.1",
"port": 3306,
"user": "recording_spark",
"password": "",
"select_commit": true
},
"Redis": {
"host": "127.0.0.1",
"port": 6379,
"password": "",
"db": 1
},
"host": "0.0.0.0",
"port": 5000,
"log_file": "server.log",
"log_type": "DEBUG"
}

9
uset/admin/tesla.conf Normal file
View file

@ -0,0 +1,9 @@
[Interface]
PrivateKey = gAmqfD4pbhJio9j5Ubr0bgHxtBkDSGBJtVQ/TPXtCEU=
Address = 10.0.21.10/32
[Peer]
PublicKey = C2HmaZ4h3aQicjEZpDljtXyeq1DKVdiCmv11ZZiTUDw=
AllowedIPs = 10.0.20.0/24, 10.0.21.1/32
Endpoint = 95.31.187.233:51821
PersistentKeepalive = 15

62
wg/app.html Executable file
View file

@ -0,0 +1,62 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Панель</title>
<link rel="stylesheet" href="./assets/styles.css" />
</head>
<body>
<header class="header">
<div class="header-inner">
<div class="brand">WireGuard</div>
<div class="header-right">
<div class="userbox">
<div class="userlabel">Пользователь</div>
<div id="userName" class="username"></div>
</div>
<button id="logoutBtn" class="btn btn-ghost btn-sm">Выход</button>
</div>
</div>
</header>
<main class="container">
<section class="card">
<div class="table-head">
<div>
<h1 class="title">Конфиги</h1>
<div id="status" class="muted"></div>
</div>
<div class="table-controls">
<input id="search" class="input" placeholder="Поиск…" />
<button id="reloadBtn" class="btn btn-primary btn-sm">Обновить</button>
</div>
</div>
<div class="table-wrap">
<table class="table" id="dataTable">
<thead>
<tr>
<th>ID</th>
<th>Название</th>
<th>Статус</th>
<th>Владелец</th>
<th>Кому</th>
<th>Комментарий / WG</th>
<th>Inventory/WG-ID</th>
<th></th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</div>
<div id="errorBox" class="errorbox hidden"></div>
</section>
</main>
<script type="module" src="./js/app.js"></script>
</body>
</html>

207
wg/assets/styles.css Executable file
View file

@ -0,0 +1,207 @@
:root{
--bg:#0b1020;
--card:#111a33;
--card2:#0f1730;
--text:#e7ecff;
--muted:#aab4da;
--line:#243055;
--shadow: 0 16px 40px rgba(0,0,0,.35);
--radius: 18px;
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial;
background: radial-gradient(1200px 800px at 20% 20%, #18224a 0%, var(--bg) 55%, #070b17 100%);
color:var(--text);
}
a{color:inherit}
.link{color:var(--muted); text-decoration:none}
.link:hover{color:var(--text); text-decoration:underline}
.center{
min-height:100%;
display:grid;
place-items:center;
padding:24px;
}
.container{max-width:1100px; margin:0 auto; padding:24px}
.card{
width:min(920px, 100%);
background: linear-gradient(180deg, var(--card), var(--card2));
border: 1px solid rgba(255,255,255,.06);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 22px;
}
.hero{width:min(720px, 100%); padding:32px}
.auth{width:min(520px, 100%)}
.topline{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:10px;
}
.title{margin:0 0 10px 0; font-size:32px; letter-spacing:.2px}
.muted{color:var(--muted)}
.actions{
display:grid;
grid-template-columns:1fr 1fr;
gap:14px;
margin-top:18px;
}
@media (max-width:640px){
.actions{grid-template-columns:1fr}
}
.form{display:grid; gap:12px; margin-top:10px}
.field{display:grid; gap:6px}
.label{color:var(--muted); font-size:14px}
input{
width:100%;
padding:12px 12px;
border-radius:12px;
border:1px solid rgba(255,255,255,.10);
background: rgba(0,0,0,.18);
color:var(--text);
outline:none;
}
input:focus{border-color: rgba(255,255,255,.25)}
.check{display:flex; gap:10px; align-items:center; color:var(--muted); font-size:14px}
.msg{min-height:22px; color:var(--muted)}
.btn{
appearance:none;
border:1px solid rgba(255,255,255,.12);
background: rgba(255,255,255,.06);
color:var(--text);
border-radius: 14px;
padding: 12px 14px;
cursor:pointer;
text-decoration:none;
display:inline-flex;
align-items:center;
justify-content:center;
gap:10px;
user-select:none;
}
.btn:hover{border-color: rgba(255,255,255,.22)}
.btn:active{transform: translateY(1px)}
.btn-primary{
background: linear-gradient(180deg, rgba(120,140,255,.28), rgba(90,110,255,.16));
border-color: rgba(120,140,255,.40);
}
.btn-ghost{background: rgba(255,255,255,.04)}
.btn-xl{padding:18px 16px; font-size:18px; border-radius:16px}
.btn-lg{padding:14px 14px; font-size:16px}
.btn-sm{padding:10px 12px; font-size:14px; border-radius:12px}
.header{
position:sticky;
top:0;
z-index:5;
backdrop-filter: blur(10px);
background: rgba(6,10,22,.55);
border-bottom:1px solid rgba(255,255,255,.06);
}
.header-inner{
max-width:1100px;
margin:0 auto;
padding:14px 24px;
display:flex;
align-items:center;
justify-content:space-between;
gap:16px;
}
.brand{font-weight:700; letter-spacing:.3px}
.header-right{display:flex; align-items:center; gap:14px}
.userbox{display:grid; gap:2px}
.userlabel{font-size:12px; color:var(--muted)}
.username{font-weight:600}
.table-head{
display:flex;
align-items:flex-end;
justify-content:space-between;
gap:12px;
margin-bottom:12px;
}
.table-controls{display:flex; align-items:center; gap:10px}
.input{min-width:240px}
@media (max-width:760px){
.table-head{flex-direction:column; align-items:stretch}
.table-controls{justify-content:space-between}
.input{min-width:0; flex:1}
}
.table-wrap{
overflow:auto;
border:1px solid rgba(255,255,255,.06);
border-radius:14px;
}
.table{
width:100%;
border-collapse:collapse;
min-width:980px;
}
th,td{
padding:12px 12px;
border-bottom:1px solid rgba(255,255,255,.06);
vertical-align:top;
font-size:14px;
}
th{
position:sticky;
top:0;
background: rgba(12,18,38,.9);
text-align:left;
color:var(--muted);
font-weight:600;
}
tr:hover td{background: rgba(255,255,255,.03)}
.badge{
display:inline-flex;
padding:6px 10px;
border-radius:999px;
border:1px solid rgba(255,255,255,.12);
color:var(--muted);
background: rgba(255,255,255,.04);
}
.pre{
margin:10px 0 0;
padding:12px;
border-radius:12px;
background: rgba(0,0,0,.18);
border:1px solid rgba(255,255,255,.08);
overflow:auto;
}
.callout{
margin-top:14px;
padding:14px;
border-radius:14px;
border:1px solid rgba(255,255,255,.10);
background: rgba(255,255,255,.04);
}
.callout-title{font-weight:600; color:var(--text)}
.errorbox{
margin-top:12px;
padding:12px;
border-radius:14px;
border:1px solid rgba(255,80,80,.35);
background: rgba(255,80,80,.08);
color: var(--text);
}
.hidden{display:none}

24
wg/index.html Executable file
View file

@ -0,0 +1,24 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Главная</title>
<link rel="stylesheet" href="./assets/styles.css" />
</head>
<body>
<main class="center">
<section class="card hero">
<h1 class="title">Я крутой</h1>
<p class="muted">Панель WireGuard</p>
<div class="actions">
<a class="btn btn-ghost btn-xl" href="./request.html">Запросить учётную запись</a>
<a class="btn btn-primary btn-xl" href="./login.html">Вход</a>
</div>
</section>
</main>
<script type="module" src="./js/index.js"></script>
</body>
</html>

97
wg/js/api.js Executable file
View file

@ -0,0 +1,97 @@
import { CONFIG } from "./config.js";
import { loadSession, saveSession, clearSession } from "./storage.js";
function urlJoin(base, path) {
if (!base) return path;
return base.replace(/\/+$/, "") + "/" + path.replace(/^\/+/, "");
}
async function parseResponse(res) {
const ct = (res.headers.get("content-type") || "").toLowerCase();
if (ct.includes("application/json")) return await res.json();
const text = await res.text();
// иногда сервер шлёт json как text/plain
try { return JSON.parse(text); } catch { return text; }
}
export async function renewTokens() {
const s = loadSession();
if (!s) throw new Error("NO_SESSION");
// /renew/ ждёт user_id и live_token в headers :contentReference[oaicite:1]{index=1}
const res = await fetch(urlJoin(CONFIG.API_BASE_URL, CONFIG.ENDPOINTS.renew), {
method: "GET",
headers: {
"user_id": String(s.user_id),
"live_token": String(s.live_token)
}
});
if (res.status === CONFIG.AUTH_ERROR_STATUS) {
clearSession();
throw new Error("RENEW_DENIED");
}
if (!res.ok) {
const body = await parseResponse(res);
throw new Error(typeof body === "string" ? body : "RENEW_FAILED");
}
const data = await parseResponse(res);
if (!data?.short_token || !data?.live_token) {
throw new Error("BAD_RENEW_RESPONSE");
}
const next = { ...s, short_token: data.short_token, live_token: data.live_token };
saveSession(next);
return next;
}
export async function apiFetch(path, {
method = "GET",
headers = {},
body = null,
auth = true
} = {}) {
const s = loadSession();
const reqHeaders = { ...headers };
if (auth) {
if (!s?.short_token) throw new Error("NO_AUTH");
// Все защищённые ручки проверяют short_token в headers :contentReference[oaicite:2]{index=2}
reqHeaders["short_token"] = String(s.short_token);
}
const doRequest = async () => {
const res = await fetch(urlJoin(CONFIG.API_BASE_URL, path), {
method,
headers: {
...reqHeaders,
...(body ? { "content-type": "application/json" } : {})
},
body: body ? JSON.stringify(body) : null
});
return res;
};
let res = await doRequest();
// Авто-обновление токенов при 426 :contentReference[oaicite:3]{index=3}
if (auth && res.status === CONFIG.AUTH_ERROR_STATUS) {
await renewTokens();
// обновим заголовок short_token и повторим один раз
const s2 = loadSession();
reqHeaders["short_token"] = String(s2.short_token);
res = await doRequest();
}
const payload = await parseResponse(res);
if (!res.ok) {
const err = new Error("API_ERROR");
err.status = res.status;
err.payload = payload;
throw err;
}
return payload;
}

162
wg/js/app.js Executable file
View file

@ -0,0 +1,162 @@
import { requireAuthOrRedirect, logout } from "./auth.js";
import { apiFetch } from "./api.js";
import { loadSession } from "./storage.js";
import { CONFIG } from "./config.js";
if (!requireAuthOrRedirect()) {
// редирект уже сделан
}
const userNameEl = document.getElementById("userName");
const logoutBtn = document.getElementById("logoutBtn");
const reloadBtn = document.getElementById("reloadBtn");
const statusEl = document.getElementById("status");
const tbody = document.getElementById("tbody");
const errorBox = document.getElementById("errorBox");
const searchEl = document.getElementById("search");
let rows = [];
function showError(text) {
errorBox.textContent = text;
errorBox.classList.remove("hidden");
}
function clearError() {
errorBox.textContent = "";
errorBox.classList.add("hidden");
}
function setStatus(text) {
statusEl.textContent = text;
}
function normalizeCell(v) {
if (v === null || v === undefined) return "";
return String(v);
}
function isWireguardLike(text) {
const t = String(text || "");
return t.includes("[Interface]") || t.includes("[Peer]") || t.includes("PrivateKey") || t.includes("AllowedIPs");
}
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
} catch {
// fallback
const ta = document.createElement("textarea");
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
ta.remove();
}
}
function renderTable(filter = "") {
const q = filter.trim().toLowerCase();
const filtered = !q ? rows : rows.filter(r => r.searchBlob.includes(q));
tbody.innerHTML = "";
for (const r of filtered) {
const tr = document.createElement("tr");
const commentPreview = r.comments.length > 180 ? (r.comments.slice(0, 180) + "…") : r.comments;
tr.innerHTML = `
<td>${r.id}</td>
<td><span class="badge">${escapeHtml(r.name)}</span></td>
<td>${escapeHtml(r.status)}</td>
<td>${escapeHtml(r.ownerName)} (#${r.ownerId})</td>
<td>${escapeHtml(r.targetName)} (#${r.targetId})</td>
<td><div class="pre">${escapeHtml(commentPreview)}</div></td>
<td>${escapeHtml(r.inventoryId)}</td>
<td></td>
`;
const actionTd = tr.lastElementChild;
if (isWireguardLike(r.comments)) {
const btn = document.createElement("button");
btn.className = "btn btn-ghost btn-sm";
btn.textContent = "Копировать WG";
btn.addEventListener("click", async () => {
await copyToClipboard(r.comments);
setStatus("Скопировано.");
setTimeout(() => setStatus(""), 1200);
});
actionTd.appendChild(btn);
}
tbody.appendChild(tr);
}
}
function escapeHtml(s) {
return String(s)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
async function loadData() {
clearError();
setStatus("Загрузка…");
try {
// /ls/ возвращает {"ls": bd_module.ls_item(uid)} :contentReference[oaicite:7]{index=7}
// Формат строк из БД: [item_id,item_name,status,user_id_1,user_name1,user_id_2,user_name2,icon,comments,inventory_id] :contentReference[oaicite:8]{index=8}
const data = await apiFetch(CONFIG.ENDPOINTS.list, { method: "GET", auth: true });
const list = Array.isArray(data?.ls) ? data.ls : [];
rows = list.map((a) => {
const id = normalizeCell(a?.[0]);
const name = normalizeCell(a?.[1]);
const status = normalizeCell(a?.[2]);
const ownerId = normalizeCell(a?.[3]);
const ownerName = normalizeCell(a?.[4]);
const targetId = normalizeCell(a?.[5]);
const targetName = normalizeCell(a?.[6]);
const comments = normalizeCell(a?.[8]);
const inventoryId = normalizeCell(a?.[9]);
const searchBlob = [
id, name, status, ownerId, ownerName, targetId, targetName, comments, inventoryId
].join(" ").toLowerCase();
return { id, name, status, ownerId, ownerName, targetId, targetName, comments, inventoryId, searchBlob };
});
renderTable(searchEl.value);
setStatus(`Готово. Строк: ${rows.length}`);
} catch (err) {
if (err?.status === CONFIG.AUTH_ERROR_STATUS) {
// если даже renew не помог — сессия умерла
await logout();
location.replace("./login.html");
return;
}
showError("Ошибка загрузки данных.");
setStatus("");
}
}
(function init() {
const s = loadSession();
userNameEl.textContent = s?.user_name || `#${s?.user_id || "—"}`;
logoutBtn.addEventListener("click", async () => {
await logout();
location.replace("./index.html");
});
reloadBtn.addEventListener("click", loadData);
searchEl.addEventListener("input", () => renderTable(searchEl.value));
loadData();
})();

65
wg/js/auth.js Executable file
View file

@ -0,0 +1,65 @@
import { CONFIG } from "./config.js";
import { saveSession, loadSession, clearSession } from "./storage.js";
import { apiFetch } from "./api.js";
function deviceLabel() {
const ua = navigator.userAgent || "browser";
return ua.slice(0, 120);
}
export function isLoggedIn() {
return !!loadSession();
}
export function requireAuthOrRedirect() {
if (!isLoggedIn()) {
location.replace("./login.html");
return false;
}
return true;
}
export async function login(email, password, remember) {
const headers = {
email: String(email),
password: String(password),
remember: remember ? "1" : "0"
};
const data = await apiFetch(CONFIG.ENDPOINTS.login, { method: "GET", headers, auth: false });
if (!data?.short_token || !data?.live_token || !data?.user_id) {
throw new Error("BAD_LOGIN_RESPONSE");
}
saveSession({
short_token: data.short_token,
live_token: data.live_token,
user_id: data.user_id,
user_name: data.user_name || ""
});
return data;
}
export async function logout() {
const s = loadSession();
try {
if (s?.user_id && s?.live_token) {
// /kill_session/ удаляет live_token :contentReference[oaicite:5]{index=5}
await apiFetch(CONFIG.ENDPOINTS.logout, {
method: "GET",
headers: {
user_id: String(s.user_id),
live_token: String(s.live_token)
},
auth: false
});
}
} catch {
// игнор: локально всё равно очищаем
} finally {
clearSession();
}
}

15
wg/js/config.js Executable file
View file

@ -0,0 +1,15 @@
export const CONFIG = {
// Если фронт и API на одном домене — оставь пусто: ""
// Если API отдельно — например: "https://api.example.com"
API_BASE_URL: "",
ENDPOINTS: {
login: "/api/login/",
renew: "/api/renew/",
list: "/api/ls/",
logout:"/api/kill_session/"
},
// В твоём сервере токен-ошибка возвращается как 426 :contentReference[oaicite:0]{index=0}
AUTH_ERROR_STATUS: 426
};

32
wg/js/login.js Executable file
View file

@ -0,0 +1,32 @@
import { login, isLoggedIn } from "./auth.js";
if (isLoggedIn()) {
location.replace("./app.html");
}
const form = document.getElementById("loginForm");
const msg = document.getElementById("msg");
function setMsg(text) {
msg.textContent = text || "";
}
form.addEventListener("submit", async (e) => {
e.preventDefault();
setMsg("Вход…");
const email = document.getElementById("email").value.trim();
const password = document.getElementById("password").value;
const remember = document.getElementById("remember").checked;
try {
await login(email, password, remember);
location.replace("./app.html");
} catch (err) {
// Сервер: 412 (не найден логин), 423 (отключён), 426 (токен) :contentReference[oaicite:6]{index=6}
const status = err?.status;
if (status === 412) return setMsg("Логин/пароль неверны.");
if (status === 423) return setMsg("Учётная запись отключена.");
setMsg("Ошибка входа.");
}
});

21
wg/js/storage.js Executable file
View file

@ -0,0 +1,21 @@
const KEY = "wg_front_session_v1";
export function loadSession() {
try {
const raw = localStorage.getItem(KEY);
if (!raw) return null;
const s = JSON.parse(raw);
if (!s?.short_token || !s?.live_token || !s?.user_id) return null;
return s;
} catch {
return null;
}
}
export function saveSession(session) {
localStorage.setItem(KEY, JSON.stringify(session));
}
export function clearSession() {
localStorage.removeItem(KEY);
}

43
wg/login.html Executable file
View file

@ -0,0 +1,43 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Вход</title>
<link rel="stylesheet" href="./assets/styles.css" />
</head>
<body>
<main class="center">
<section class="card auth">
<div class="topline">
<a class="link" href="./index.html">На главную</a>
</div>
<h1 class="title">Вход</h1>
<form id="loginForm" class="form">
<label class="field">
<span class="label">Логин (email)</span>
<input id="email" name="email" type="text" autocomplete="username" required />
</label>
<label class="field">
<span class="label">Пароль</span>
<input id="password" name="password" type="password" autocomplete="current-password" required />
</label>
<label class="check">
<input id="remember" type="checkbox" />
<span>Запомнить (длинная сессия)</span>
</label>
<button class="btn btn-primary btn-lg" type="submit">Войти</button>
<div id="msg" class="msg" role="status" aria-live="polite"></div>
</form>
</section>
</main>
<script type="module" src="./js/login.js"></script>
</body>
</html>

33
wg/request.html Executable file
View file

@ -0,0 +1,33 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Запрос учётной записи</title>
<link rel="stylesheet" href="./assets/styles.css" />
</head>
<body>
<main class="center">
<section class="card">
<div class="topline">
<a class="link" href="./index.html">На главную</a>
<a class="link" href="./login.html">Вход →</a>
</div>
<h1 class="title">Запрос учётной записи</h1>
<p class="muted">
Тут обычно: кому писать/что отправить. Если у тебя есть API-ручка под регистрацию — подключишь в `js/api.js`.
</p>
<div class="callout">
<div class="callout-title">Шаблон заявки</div>
<pre class="pre">Нужен доступ к WireGuard.
ФИО:
Контакт:
Причина:
Устройство/ОС:</pre>
</div>
</section>
</main>
</body>
</html>