mirror of
https://github.com/qwerfd2/Groove_Coaster_2_Server.git
synced 2025-12-21 19:20:11 +00:00
358 lines
15 KiB
Python
358 lines
15 KiB
Python
from starlette.requests import Request
|
|
from starlette.routing import Route
|
|
from starlette.responses import HTMLResponse, JSONResponse, RedirectResponse
|
|
import sqlalchemy
|
|
import json
|
|
import datetime
|
|
|
|
from api.database import player_database, accounts, results, devices, whitelists, blacklists, batch_tokens, binds, webs, logs, is_admin
|
|
from api.misc import crc32_decimal, read_user_save_file, write_user_save_file
|
|
|
|
TABLE_MAP = {
|
|
"accounts": (accounts, ["id", "username", "password_hash", "save_crc", "save_timestamp", "save_id", "coin_mp", "title", "avatar", "created_at", "updated_at"]),
|
|
"results": (results, ["id", "device_id", "stts", "song_id", "mode", "avatar", "score", "high_score", "play_rslt", "item", "os", "os_ver", "ver", "created_at", "updated_at"]),
|
|
"devices": (devices, ["device_id", "user_id", "my_stage", "my_avatar", "item", "daily_day", "coin", "lvl", "title", "avatar", "mobile_sum", "arcade_sum", "total_sum", "created_at", "updated_at", "last_login_at"]),
|
|
"whitelist": (whitelists, ["id", "device_id"]),
|
|
"blacklist": (blacklists, ["id", "ban_terms", "reason"]),
|
|
"batch_tokens": (batch_tokens, ["id", "bind_token", "expire_at", "uses_left", "auth_id", "created_at", "updated_at"]),
|
|
"binds": (binds, ["id", "user_id", "bind_account", "bind_code", "is_verified", "bind_token", "created_at", "updated_at"]),
|
|
"webs": (webs, ["id", "user_id", "permission", "web_token", "created_at", "updated_at"]),
|
|
"logs": (logs, ["id", "user_id", "filename", "filesize", "timestamp"]),
|
|
}
|
|
|
|
async def web_admin_page(request: Request):
|
|
adm = await is_admin(request.cookies.get("token"))
|
|
if not adm:
|
|
response = RedirectResponse(url="/login")
|
|
response.delete_cookie("token")
|
|
return response
|
|
with open("web/admin.html", "r", encoding="utf-8") as file:
|
|
html_template = file.read()
|
|
return HTMLResponse(content=html_template)
|
|
|
|
def serialize_row(row, allowed_fields):
|
|
result = {}
|
|
for field in allowed_fields:
|
|
value = row[field]
|
|
if hasattr(value, "isoformat"): # Check if it's a datetime object
|
|
result[field] = value.isoformat()
|
|
else:
|
|
result[field] = value
|
|
return result
|
|
|
|
async def web_admin_get_table(request: Request):
|
|
params = request.query_params
|
|
adm = await is_admin(request.cookies.get("token"))
|
|
if not adm:
|
|
return JSONResponse({"data": [], "last_page": 1, "total": 0}, status_code=400)
|
|
|
|
table_name = params.get("table")
|
|
page = int(params.get("page", 1))
|
|
size = int(params.get("size", 25))
|
|
sort = params.get("sort")
|
|
dir_ = params.get("dir", "asc")
|
|
search = params.get("search", "").strip()
|
|
schema = params.get("schema", "0") == "1"
|
|
|
|
if schema:
|
|
table, _ = TABLE_MAP[table_name]
|
|
columns = table.columns # This is a ColumnCollection
|
|
schema = {col.name: str(col.type).upper() for col in columns}
|
|
return JSONResponse(schema)
|
|
|
|
# Validate table
|
|
if table_name not in TABLE_MAP:
|
|
return JSONResponse({"data": [], "last_page": 1, "total": 0}, status_code=400)
|
|
|
|
# Validate size
|
|
if size < 10:
|
|
size = 10
|
|
if size > 100:
|
|
size = 100
|
|
|
|
table, allowed_fields = TABLE_MAP[table_name]
|
|
|
|
# Build query
|
|
query = table.select()
|
|
|
|
# Search
|
|
if search:
|
|
search_clauses = []
|
|
for field in allowed_fields:
|
|
col = getattr(table.c, field, None)
|
|
if col is not None:
|
|
search_clauses.append(col.like(f"%{search}%"))
|
|
if search_clauses:
|
|
query = query.where(sqlalchemy.or_(*search_clauses))
|
|
|
|
# Sort
|
|
if sort in allowed_fields:
|
|
col = getattr(table.c, sort, None)
|
|
if col is not None:
|
|
if isinstance(col.type, sqlalchemy.types.String):
|
|
if dir_ == "desc":
|
|
query = query.order_by(sqlalchemy.func.lower(col).desc())
|
|
else:
|
|
query = query.order_by(sqlalchemy.func.lower(col).asc())
|
|
else:
|
|
if dir_ == "desc":
|
|
query = query.order_by(col.desc())
|
|
else:
|
|
query = query.order_by(col.asc())
|
|
|
|
# Pagination
|
|
offset = (page - 1) * size
|
|
query = query.offset(offset).limit(size)
|
|
|
|
# total count
|
|
count_query = sqlalchemy.select(sqlalchemy.func.count()).select_from(table)
|
|
if search:
|
|
search_clauses = []
|
|
for field in allowed_fields:
|
|
col = getattr(table.c, field, None)
|
|
if col is not None:
|
|
search_clauses.append(col.like(f"%{search}%"))
|
|
if search_clauses:
|
|
count_query = count_query.where(sqlalchemy.or_(*search_clauses))
|
|
total = await player_database.fetch_val(count_query)
|
|
last_page = max(1, (total + size - 1) // size)
|
|
|
|
# Fetch data
|
|
rows = await player_database.fetch_all(query)
|
|
data = [serialize_row(row, allowed_fields) for row in rows]
|
|
|
|
response_data = {"data": data, "last_page": last_page, "total": total}
|
|
|
|
return JSONResponse(response_data)
|
|
|
|
async def web_admin_table_set(request: Request):
|
|
params = await request.json()
|
|
adm = await is_admin(request.cookies.get("token"))
|
|
if not adm:
|
|
return JSONResponse({"status": "failed", "message": "Invalid token."}, status_code=400)
|
|
|
|
table_name = params.get("table")
|
|
row = params.get("row")
|
|
|
|
if table_name not in TABLE_MAP:
|
|
return JSONResponse({"status": "failed", "message": "Invalid table name."}, status_code=401)
|
|
|
|
table, _ = TABLE_MAP[table_name]
|
|
columns = table.columns # This is a ColumnCollection
|
|
|
|
schema = {col.name: str(col.type) for col in columns}
|
|
|
|
try:
|
|
row_data = row
|
|
if not isinstance(row_data, dict):
|
|
raise ValueError("Row data must be a JSON object.")
|
|
id_field = None
|
|
# Find primary key field (id or objectId)
|
|
for pk in ["id"]:
|
|
if pk in row_data:
|
|
id_field = pk
|
|
break
|
|
if not id_field:
|
|
raise ValueError("Row data must contain a primary key ('id' or 'objectId').")
|
|
for key, value in row_data.items():
|
|
if key not in schema:
|
|
raise ValueError(f"Field '{key}' does not exist in table schema.")
|
|
# Type checking
|
|
expected_type = schema[key]
|
|
if (value == "" or value is None) and table.c[key].nullable:
|
|
continue # Allow null or empty for nullable fields
|
|
|
|
if expected_type.startswith("INTEGER"):
|
|
if not isinstance(value, int):
|
|
try:
|
|
value = int(value)
|
|
except:
|
|
raise ValueError(f"Field '{key}' must be an integer.")
|
|
|
|
elif expected_type.startswith("FLOAT"):
|
|
try:
|
|
value = float(value)
|
|
except:
|
|
raise ValueError(f"Field '{key}' must be a float.")
|
|
|
|
elif expected_type.startswith("BOOLEAN"):
|
|
try:
|
|
if isinstance(value, str):
|
|
if value.lower() in ["true", "1"]:
|
|
value = True
|
|
elif value.lower() in ["false", "0"]:
|
|
value = False
|
|
else:
|
|
raise ValueError
|
|
elif isinstance(value, int):
|
|
value = bool(value)
|
|
except:
|
|
raise ValueError(f"Field '{key}' must be a boolean.")
|
|
|
|
elif expected_type.startswith("JSON"):
|
|
if not isinstance(value, dict) and not isinstance(value, list):
|
|
raise ValueError(f"Field '{key}' must be a JSON object or array.")
|
|
elif expected_type.startswith("VARCHAR") or expected_type.startswith("STRING"):
|
|
if not isinstance(value, str):
|
|
raise ValueError(f"Field '{key}' must be a string.")
|
|
elif expected_type.startswith("DATETIME"):
|
|
# Try to convert to datetime object
|
|
try:
|
|
if isinstance(value, str):
|
|
dt_obj = datetime.datetime.fromisoformat(value)
|
|
row_data[key] = dt_obj
|
|
elif isinstance(value, (int, float)):
|
|
dt_obj = datetime.datetime.fromtimestamp(value)
|
|
row_data[key] = dt_obj
|
|
elif isinstance(value, datetime.datetime):
|
|
pass # already datetime
|
|
else:
|
|
raise ValueError
|
|
except Exception:
|
|
raise ValueError(f"Field '{key}' must be a valid ISO datetime string or timestamp.")
|
|
except Exception as e:
|
|
return JSONResponse({"status": "failed", "message": f"Invalid row data: {str(e)}"}, status_code=402)
|
|
|
|
update_data = {k: v for k, v in row_data.items() if k != id_field}
|
|
for upd_data in update_data:
|
|
if upd_data == "" or upd_data is None:
|
|
if not table.c[upd_data].nullable:
|
|
return JSONResponse({"status": "failed", "message": f"Field '{upd_data}' cannot be null."}, status_code=403)
|
|
else:
|
|
update_data[upd_data] = None
|
|
update_query = table.update().where(getattr(table.c, id_field) == row_data[id_field]).values(**update_data)
|
|
await player_database.execute(update_query)
|
|
|
|
return JSONResponse({"status": "success", "message": "Row updated successfully."})
|
|
|
|
async def web_admin_table_delete(request: Request):
|
|
params = await request.json()
|
|
adm = await is_admin(request.cookies.get("token"))
|
|
if not adm:
|
|
return JSONResponse({"status": "failed", "message": "Invalid token."}, status_code=400)
|
|
|
|
table_name = params.get("table")
|
|
row_id = params.get("id")
|
|
|
|
if table_name not in TABLE_MAP:
|
|
return JSONResponse({"status": "failed", "message": "Invalid table name."}, status_code=401)
|
|
|
|
if not row_id:
|
|
return JSONResponse({"status": "failed", "message": "Row ID is required."}, status_code=402)
|
|
|
|
table, _ = TABLE_MAP[table_name]
|
|
|
|
if table_name in ["results"]:
|
|
delete_query = table.delete().where(table.c.rid == row_id)
|
|
else:
|
|
delete_query = table.delete().where(table.c.id == row_id)
|
|
|
|
result = await player_database.execute(delete_query)
|
|
|
|
return JSONResponse({"status": "success", "message": "Row deleted successfully."})
|
|
|
|
async def web_admin_table_insert(request: Request):
|
|
params = await request.json()
|
|
adm = await is_admin(request.cookies.get("token"))
|
|
if not adm:
|
|
return JSONResponse({"status": "failed", "message": "Invalid token."}, status_code=400)
|
|
|
|
table_name = params.get("table")
|
|
row = params.get("row")
|
|
|
|
if table_name not in TABLE_MAP:
|
|
return JSONResponse({"status": "failed", "message": "Invalid table name."}, status_code=401)
|
|
|
|
table, _ = TABLE_MAP[table_name]
|
|
columns = table.columns
|
|
|
|
schema = {col.name: str(col.type) for col in columns}
|
|
|
|
# VERIFY that the row data conforms to the schema
|
|
try:
|
|
row_data = row
|
|
if not isinstance(row_data, dict):
|
|
raise ValueError("Row data must be a JSON object.")
|
|
for key, value in row_data.items():
|
|
if key not in schema:
|
|
raise ValueError(f"Field '{key}' does not exist in table schema.")
|
|
expected_type = schema[key]
|
|
if expected_type.startswith("INTEGER"):
|
|
if not isinstance(value, int):
|
|
raise ValueError(f"Field '{key}' must be an integer.")
|
|
elif expected_type.startswith("FLOAT"):
|
|
if not isinstance(value, float) and not isinstance(value, int):
|
|
raise ValueError(f"Field '{key}' must be a float.")
|
|
elif expected_type.startswith("BOOLEAN"):
|
|
if not isinstance(value, bool):
|
|
raise ValueError(f"Field '{key}' must be a boolean.")
|
|
elif expected_type.startswith("JSON"):
|
|
try:
|
|
json.loads(value)
|
|
except:
|
|
raise ValueError(f"Field '{key}' must be a valid JSON string.")
|
|
elif expected_type.startswith("VARCHAR") or expected_type.startswith("STRING"):
|
|
if not isinstance(value, str):
|
|
raise ValueError(f"Field '{key}' must be a string.")
|
|
elif expected_type.startswith("DATETIME"):
|
|
try:
|
|
if isinstance(value, str):
|
|
dt_obj = datetime.datetime.fromisoformat(value)
|
|
row_data[key] = dt_obj
|
|
elif isinstance(value, (int, float)):
|
|
dt_obj = datetime.datetime.fromtimestamp(value)
|
|
row_data[key] = dt_obj
|
|
elif isinstance(value, datetime.datetime):
|
|
pass
|
|
else:
|
|
raise ValueError
|
|
except Exception:
|
|
raise ValueError(f"Field '{key}' must be a valid ISO datetime string or timestamp.")
|
|
except Exception as e:
|
|
return JSONResponse({"status": "failed", "message": f"Invalid row data: {str(e)}"}, status_code=402)
|
|
|
|
insert_data = {k: v for k, v in row_data.items() if k in schema}
|
|
insert_query = table.insert().values(**insert_data)
|
|
result = await player_database.execute(insert_query)
|
|
return JSONResponse({"status": "success", "message": "Row inserted successfully.", "inserted_id": result})
|
|
|
|
async def web_admin_data_get(request: Request):
|
|
adm = await is_admin(request.cookies.get("token"))
|
|
if not adm:
|
|
return JSONResponse({"status": "failed", "message": "Invalid token."}, status_code=400)
|
|
|
|
params = request.query_params
|
|
uid = int(params.get("id"))
|
|
|
|
data = await read_user_save_file(uid)
|
|
|
|
return JSONResponse({"status": "success", "data": data})
|
|
|
|
async def web_admin_data_save(request: Request):
|
|
adm = await is_admin(request.cookies.get("token"))
|
|
if not adm:
|
|
return JSONResponse({"status": "failed", "message": "Invalid token."}, status_code=400)
|
|
|
|
params = await request.json()
|
|
uid = int(params['id'])
|
|
save_data = params['data']
|
|
|
|
crc = crc32_decimal(save_data)
|
|
formatted_time = datetime.datetime.now()
|
|
|
|
query = accounts.update().where(accounts.c.id == uid).values(save_crc=crc, save_timestamp=formatted_time)
|
|
await player_database.execute(query)
|
|
await write_user_save_file(uid, save_data)
|
|
|
|
return JSONResponse({"status": "success", "message": "Data saved successfully."})
|
|
|
|
routes = [
|
|
Route("/admin", web_admin_page, methods=["GET"]),
|
|
Route("/admin/", web_admin_page, methods=["GET"]),
|
|
Route("/admin/table", web_admin_get_table, methods=["GET"]),
|
|
Route("/admin/table/update", web_admin_table_set, methods=["POST"]),
|
|
Route("/admin/table/delete", web_admin_table_delete, methods=["POST"]),
|
|
Route("/admin/table/insert", web_admin_table_insert, methods=["POST"]),
|
|
Route("/admin/data", web_admin_data_get, methods=["GET"]),
|
|
Route("/admin/data/save", web_admin_data_save, methods=["POST"]),
|
|
] |