fix
This commit is contained in:
commit
02f3a1b884
32 changed files with 5597 additions and 0 deletions
476
Start.py
Normal file
476
Start.py
Normal 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"])
|
||||
BIN
__pycache__/bd_module.cpython-313.pyc
Normal file
BIN
__pycache__/bd_module.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/db_module.cpython-313.pyc
Normal file
BIN
__pycache__/db_module.cpython-313.pyc
Normal file
Binary file not shown.
1593
bd_module)_OLD.py
Normal file
1593
bd_module)_OLD.py
Normal file
File diff suppressed because it is too large
Load diff
748
bd_module.py
Normal file
748
bd_module.py
Normal 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
BIN
data.sqlite3
Normal file
Binary file not shown.
32
db_module.py
Normal file
32
db_module.py
Normal 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
60
frontend/app.html
Executable 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
207
frontend/assets/styles.css
Executable 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
24
frontend/index.html
Executable 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
97
frontend/js/api.js
Executable 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
207
frontend/js/app.js
Executable 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("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
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
65
frontend/js/auth.js
Executable 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
16
frontend/js/config.js
Executable 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
32
frontend/js/login.js
Executable 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
21
frontend/js/storage.js
Executable 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
43
frontend/login.html
Executable 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
33
frontend/request.html
Executable 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
1153
server.log
Normal file
File diff suppressed because it is too large
Load diff
20
settings.json
Normal file
20
settings.json
Normal 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
9
uset/admin/tesla.conf
Normal 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
62
wg/app.html
Executable 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
207
wg/assets/styles.css
Executable 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
24
wg/index.html
Executable 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
97
wg/js/api.js
Executable 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
162
wg/js/app.js
Executable 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("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
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
65
wg/js/auth.js
Executable 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
15
wg/js/config.js
Executable 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
32
wg/js/login.js
Executable 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
21
wg/js/storage.js
Executable 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
43
wg/login.html
Executable 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
33
wg/request.html
Executable 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue