JCEF migration: add jcefmaven dep, make JavaFX optional, create LogBridge, wire JCEFLauncher entry point
This commit is contained in:
+216
-7
@@ -43,6 +43,32 @@ class BanUserRequest(BaseModel):
|
||||
days: int = Field(..., ge=1, le=365)
|
||||
reason: str
|
||||
|
||||
class CreatePassCodeRequest(BaseModel):
|
||||
expires_days: Optional[int] = Field(None, ge=1, le=365)
|
||||
max_uses: int = Field(1, ge=1, le=10)
|
||||
|
||||
class UpdatePassRequest(BaseModel):
|
||||
code: str
|
||||
expires_days: Optional[int] = None
|
||||
max_uses: int = Field(1, ge=1, le=10)
|
||||
is_active: bool = True
|
||||
|
||||
class DeletePassRequest(BaseModel):
|
||||
code: str
|
||||
|
||||
class CreatePassRequest(BaseModel):
|
||||
expires_days: Optional[int] = Field(None, ge=1, le=365)
|
||||
max_uses: int = Field(1, ge=1, le=10)
|
||||
|
||||
class UpdatePassRequest(BaseModel):
|
||||
code: str
|
||||
expires_days: Optional[int] = None
|
||||
max_uses: int = Field(1, ge=1, le=10)
|
||||
is_active: bool = True
|
||||
|
||||
class DeletePassRequest(BaseModel):
|
||||
code: str
|
||||
|
||||
# ====================== ЭНДПОИНТЫ ======================
|
||||
|
||||
@router.get("/users")
|
||||
@@ -532,11 +558,17 @@ async def get_admin_stats(
|
||||
recent_registrations = conn.execute("""
|
||||
SELECT COUNT(*) as count FROM users WHERE created_at > ?
|
||||
""", (week_ago,)).fetchone()["count"]
|
||||
|
||||
|
||||
# Онлайн пользователи
|
||||
online_now = conn.execute("""
|
||||
SELECT COUNT(*) as count FROM user_status WHERE is_online = 1
|
||||
""").fetchone()["count"]
|
||||
|
||||
return {
|
||||
"total_users": total_users,
|
||||
"active_passes": active_passes,
|
||||
"banned_users": banned_users,
|
||||
"online_now": online_now,
|
||||
"recent_registrations_7d": recent_registrations,
|
||||
"roles_distribution": [
|
||||
{"role": r["role"], "role_name": ROLE_NAMES.get(r["role"], "Неизвестно"), "count": r["count"]}
|
||||
@@ -550,6 +582,85 @@ async def get_admin_stats(
|
||||
}
|
||||
|
||||
|
||||
# ====================== PASS CRUD ======================
|
||||
|
||||
@router.post("/passes/create")
|
||||
async def admin_create_pass(
|
||||
body: CreatePassCodeRequest,
|
||||
current_user: dict = Depends(require_role(ROLE_ELDER)),
|
||||
request: Request = None,
|
||||
):
|
||||
"""Create a new pass code"""
|
||||
ip = request.client.host if request.client else "unknown"
|
||||
pass_code = secrets.token_hex(12).upper()
|
||||
now = time.time()
|
||||
expires_at = now + (body.expires_days * 86400) if body.expires_days else None
|
||||
|
||||
with get_db() as conn:
|
||||
conn.execute("""
|
||||
INSERT INTO passes (code, owner, expires_at, max_uses, is_active, uses, activated_at)
|
||||
VALUES (?, ?, ?, ?, 1, 0, ?)
|
||||
""", (pass_code, 'admin', expires_at, body.max_uses, now))
|
||||
conn.commit()
|
||||
|
||||
log_audit(current_user["id"], "pass_create",
|
||||
f"Created pass {pass_code[:8]}... (expires: {body.expires_days}d, max_uses: {body.max_uses})", ip)
|
||||
|
||||
return {"success": True, "code": pass_code, "expires_at": expires_at, "max_uses": body.max_uses}
|
||||
|
||||
|
||||
@router.put("/passes/update")
|
||||
async def admin_update_pass(
|
||||
body: UpdatePassRequest,
|
||||
current_user: dict = Depends(require_role(ROLE_ELDER)),
|
||||
request: Request = None,
|
||||
):
|
||||
"""Update an existing pass"""
|
||||
ip = request.client.host if request.client else "unknown"
|
||||
|
||||
with get_db() as conn:
|
||||
existing = conn.execute("SELECT code FROM passes WHERE code = ?", (body.code,)).fetchone()
|
||||
if not existing:
|
||||
raise HTTPException(404, "Pass not found")
|
||||
|
||||
expires_at = time.time() + (body.expires_days * 86400) if body.expires_days else None
|
||||
|
||||
conn.execute("""
|
||||
UPDATE passes SET expires_at = ?, max_uses = ?, is_active = ?
|
||||
WHERE code = ?
|
||||
""", (expires_at, body.max_uses, 1 if body.is_active else 0, body.code))
|
||||
conn.commit()
|
||||
|
||||
log_audit(current_user["id"], "pass_update",
|
||||
f"Updated pass {body.code[:8]}... (active: {body.is_active})", ip)
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.post("/passes/delete")
|
||||
async def admin_delete_pass(
|
||||
body: DeletePassRequest,
|
||||
current_user: dict = Depends(require_role(ROLE_ELDER)),
|
||||
request: Request = None,
|
||||
):
|
||||
"""Delete (deactivate) a pass"""
|
||||
ip = request.client.host if request.client else "unknown"
|
||||
|
||||
with get_db() as conn:
|
||||
existing = conn.execute("SELECT code FROM passes WHERE code = ?", (body.code,)).fetchone()
|
||||
if not existing:
|
||||
raise HTTPException(404, "Pass not found")
|
||||
|
||||
conn.execute("UPDATE passes SET is_active = 0 WHERE code = ?", (body.code,))
|
||||
conn.execute("DELETE FROM user_passes WHERE pass_code = ?", (body.code,))
|
||||
conn.commit()
|
||||
|
||||
log_audit(current_user["id"], "pass_delete",
|
||||
f"Deleted pass {body.code[:8]}...", ip)
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
# ====================== WHITELIST MODS ======================
|
||||
|
||||
WHITELIST_DIR = Path(__file__).parent / "whitelist"
|
||||
@@ -656,15 +767,21 @@ async def remove_whitelist_mod(
|
||||
|
||||
@router.get("/clients")
|
||||
async def admin_list_clients(current_user: dict = Depends(require_role(ROLE_MODERATOR))):
|
||||
"""List online clients"""
|
||||
"""List online clients with 5-minute stale timeout"""
|
||||
with get_db() as conn:
|
||||
# Auto-clean stale online status (no heartbeat > 5 min)
|
||||
stale_cutoff = time.time() - 300
|
||||
conn.execute("UPDATE user_status SET is_online = 0 WHERE last_seen < ?", (stale_cutoff,))
|
||||
conn.commit()
|
||||
|
||||
rows = conn.execute("""
|
||||
SELECT u.id, u.username, us.is_online, us.current_pack, us.last_seen, s.ip_address
|
||||
SELECT u.id, u.username, us.is_online, us.current_pack, us.last_seen,
|
||||
(SELECT s.ip_address FROM user_sessions s
|
||||
WHERE s.user_id = u.id AND s.is_active = 1
|
||||
ORDER BY s.created_at DESC LIMIT 1) as ip_address
|
||||
FROM users u
|
||||
LEFT JOIN user_status us ON u.id = us.user_id
|
||||
LEFT JOIN user_sessions s ON u.id = s.user_id AND s.is_active = 1
|
||||
JOIN user_status us ON u.id = us.user_id
|
||||
WHERE us.is_online = 1
|
||||
GROUP BY u.id
|
||||
ORDER BY us.last_seen DESC
|
||||
""").fetchall()
|
||||
clients = []
|
||||
@@ -672,7 +789,7 @@ async def admin_list_clients(current_user: dict = Depends(require_role(ROLE_MODE
|
||||
clients.append({
|
||||
"id": row["id"],
|
||||
"username": row["username"],
|
||||
"online": bool(row["is_online"]),
|
||||
"online": True,
|
||||
"current_pack": row["current_pack"] or "",
|
||||
"last_seen": str(row["last_seen"]) if row["last_seen"] else None,
|
||||
"ip": row["ip_address"] or ""
|
||||
@@ -811,6 +928,98 @@ async def admin_list_passes(current_user: dict = Depends(require_role(ROLE_MODER
|
||||
return {"success": True, "passes": passes}
|
||||
|
||||
|
||||
@router.post("/passes/create")
|
||||
async def create_pass(
|
||||
body: CreatePassRequest,
|
||||
current_user: dict = Depends(require_role(ROLE_ELDER)),
|
||||
request: Request = None
|
||||
):
|
||||
ip = request.client.host if request.client else "unknown"
|
||||
pass_code = secrets.token_hex(12).upper()
|
||||
now = time.time()
|
||||
expires_at = now + (body.expires_days * 86400) if body.expires_days else None
|
||||
|
||||
with get_db() as conn:
|
||||
conn.execute("""
|
||||
INSERT INTO passes (code, owner, expires_at, max_uses, is_active, uses, activated_by, activated_at)
|
||||
VALUES (?, 'admin', ?, ?, 1, 0, NULL, ?)
|
||||
""", (pass_code, expires_at, body.max_uses, now))
|
||||
conn.commit()
|
||||
|
||||
log_audit(
|
||||
current_user["id"],
|
||||
"pass_create",
|
||||
f"Created pass code {pass_code} (expires_days: {body.expires_days}, max_uses: {body.max_uses})",
|
||||
ip
|
||||
)
|
||||
|
||||
return {"success": True, "code": pass_code}
|
||||
|
||||
|
||||
@router.put("/passes/update")
|
||||
async def update_pass(
|
||||
body: UpdatePassRequest,
|
||||
current_user: dict = Depends(require_role(ROLE_ELDER)),
|
||||
request: Request = None
|
||||
):
|
||||
ip = request.client.host if request.client else "unknown"
|
||||
|
||||
with get_db() as conn:
|
||||
existing = conn.execute(
|
||||
"SELECT code FROM passes WHERE code = ?", (body.code,)
|
||||
).fetchone()
|
||||
if not existing:
|
||||
raise HTTPException(404, "Pass not found")
|
||||
|
||||
expires_at = None
|
||||
if body.expires_days is not None:
|
||||
expires_at = time.time() + (body.expires_days * 86400)
|
||||
|
||||
conn.execute("""
|
||||
UPDATE passes SET expires_at = ?, max_uses = ?, is_active = ?
|
||||
WHERE code = ?
|
||||
""", (expires_at, body.max_uses, 1 if body.is_active else 0, body.code))
|
||||
conn.commit()
|
||||
|
||||
log_audit(
|
||||
current_user["id"],
|
||||
"pass_update",
|
||||
f"Updated pass {body.code} (expires_days: {body.expires_days}, max_uses: {body.max_uses}, is_active: {body.is_active})",
|
||||
ip
|
||||
)
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.post("/passes/delete")
|
||||
async def delete_pass(
|
||||
body: DeletePassRequest,
|
||||
current_user: dict = Depends(require_role(ROLE_ELDER)),
|
||||
request: Request = None
|
||||
):
|
||||
ip = request.client.host if request.client else "unknown"
|
||||
|
||||
with get_db() as conn:
|
||||
existing = conn.execute(
|
||||
"SELECT code FROM passes WHERE code = ?", (body.code,)
|
||||
).fetchone()
|
||||
if not existing:
|
||||
raise HTTPException(404, "Pass not found")
|
||||
|
||||
conn.execute("UPDATE passes SET is_active = 0 WHERE code = ?", (body.code,))
|
||||
conn.execute("DELETE FROM user_passes WHERE pass_code = ?", (body.code,))
|
||||
conn.commit()
|
||||
|
||||
log_audit(
|
||||
current_user["id"],
|
||||
"pass_delete",
|
||||
f"Deactivated pass {body.code}",
|
||||
ip
|
||||
)
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.post("/whitelist/mods/{mod_hash}/delete")
|
||||
async def admin_delete_whitelist_mod_adapter(
|
||||
mod_hash: str,
|
||||
|
||||
Reference in New Issue
Block a user