Files
Steam-Panel-Simple/app/api/accounts.py
T
Manchik 9b7001407f v3.0.4
2026-04-25 04:17:13 +03:00

372 lines
13 KiB
Python

import json
from pathlib import Path
from fastapi import APIRouter, HTTPException, UploadFile, File
from loguru import logger
from app.database import get_db
from app.models import (
AccountCreate,
AccountOut,
AccountUpdate,
BulkImportResult,
MafileData,
)
from app.config import settings
router = APIRouter(prefix="/api/accounts", tags=["accounts"])
@router.get("", response_model=list[AccountOut])
async def list_accounts():
db = await get_db()
cursor = await db.execute("SELECT * FROM accounts ORDER BY id DESC")
rows = await cursor.fetchall()
return [dict(r) for r in rows]
@router.get("/{account_id}", response_model=AccountOut)
async def get_account(account_id: int):
db = await get_db()
cursor = await db.execute("SELECT * FROM accounts WHERE id = ?", (account_id,))
row = await cursor.fetchone()
if not row:
raise HTTPException(status_code=404, detail="Account not found")
return dict(row)
@router.post("", response_model=AccountOut, status_code=201)
async def create_account(account: AccountCreate):
db = await get_db()
cursor = await db.execute(
"""INSERT INTO accounts (login, password, steam_id, email, email_password, phone, proxy, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(
account.login,
account.password,
account.steam_id,
account.email,
account.email_password,
account.phone,
account.proxy,
account.notes,
),
)
await db.commit()
new_cursor = await db.execute(
"SELECT * FROM accounts WHERE id = ?", (cursor.lastrowid,)
)
return dict(await new_cursor.fetchone())
@router.put("/{account_id}", response_model=AccountOut)
async def update_account(account_id: int, account: AccountUpdate):
db = await get_db()
fields = {k: v for k, v in account.model_dump().items() if v is not None}
# Allow clearing fields by sending empty string → store as NULL
for k, v in account.model_dump().items():
if v == "" and k not in fields:
fields[k] = None
if not fields:
raise HTTPException(status_code=400, detail="No fields to update")
fields["updated_at"] = "datetime('now')"
set_clause = ", ".join(
f"{k} = datetime('now')" if k == "updated_at" else f"{k} = ?"
for k in fields
)
values = [v for k, v in fields.items() if k != "updated_at"]
values.append(account_id)
await db.execute(
f"UPDATE accounts SET {set_clause} WHERE id = ?",
values,
)
await db.commit()
cursor = await db.execute("SELECT * FROM accounts WHERE id = ?", (account_id,))
row = await cursor.fetchone()
if not row:
raise HTTPException(status_code=404, detail="Account not found")
return dict(row)
@router.delete("/{account_id}", status_code=204)
async def delete_account(account_id: int):
db = await get_db()
cursor = await db.execute("SELECT mafile_path FROM accounts WHERE id = ?", (account_id,))
row = await cursor.fetchone()
if not row:
raise HTTPException(status_code=404, detail="Account not found")
if row["mafile_path"]:
mafile = Path(row["mafile_path"])
if mafile.exists():
mafile.unlink()
await db.execute("DELETE FROM accounts WHERE id = ?", (account_id,))
await db.commit()
@router.post("/delete-bulk", status_code=200)
async def delete_accounts_bulk(request: dict):
"""Delete multiple accounts by IDs list, or all if ids is empty."""
db = await get_db()
ids = request.get("ids", [])
if ids:
placeholders = ",".join("?" for _ in ids)
cursor = await db.execute(
f"SELECT id, mafile_path FROM accounts WHERE id IN ({placeholders})", ids
)
else:
cursor = await db.execute("SELECT id, mafile_path FROM accounts")
rows = await cursor.fetchall()
count = 0
for row in rows:
if row["mafile_path"]:
mafile = Path(row["mafile_path"])
if mafile.exists():
mafile.unlink()
count += 1
if ids:
placeholders = ",".join("?" for _ in ids)
await db.execute(f"DELETE FROM accounts WHERE id IN ({placeholders})", ids)
else:
await db.execute("DELETE FROM accounts")
await db.commit()
return {"deleted": count}
@router.post("/assign-proxies")
async def assign_proxies_round_robin():
"""Round-robin assign proxies to accounts that don't have one."""
db = await get_db()
proxy_cursor = await db.execute("SELECT id, address, protocol FROM proxies ORDER BY id")
proxies = [dict(r) for r in await proxy_cursor.fetchall()]
if not proxies:
raise HTTPException(status_code=400, detail="No proxies available")
acc_cursor = await db.execute("SELECT id FROM accounts WHERE proxy IS NULL OR proxy = '' ORDER BY id")
account_ids = [r["id"] for r in await acc_cursor.fetchall()]
if not account_ids:
return {"assigned": 0, "proxies_used": 0}
for i, acc_id in enumerate(account_ids):
proxy = proxies[i % len(proxies)]
await db.execute(
"UPDATE accounts SET proxy = ?, updated_at = datetime('now') WHERE id = ?",
(proxy["address"], acc_id),
)
await db.commit()
from app.core.proxy_manager import proxy_manager
await proxy_manager.load()
logger.info(f"Assigned {len(proxies)} proxies to {len(account_ids)} accounts (round-robin)")
return {"assigned": len(account_ids), "proxies_used": len(proxies)}
@router.post("/reassign-proxies")
async def reassign_proxies_round_robin():
"""Round-robin reassign proxies to ALL accounts, overwriting existing."""
db = await get_db()
proxy_cursor = await db.execute("SELECT id, address, protocol FROM proxies ORDER BY id")
proxies = [dict(r) for r in await proxy_cursor.fetchall()]
if not proxies:
raise HTTPException(status_code=400, detail="No proxies available")
acc_cursor = await db.execute("SELECT id FROM accounts ORDER BY id")
account_ids = [r["id"] for r in await acc_cursor.fetchall()]
if not account_ids:
return {"assigned": 0, "proxies_used": 0}
for i, acc_id in enumerate(account_ids):
proxy = proxies[i % len(proxies)]
await db.execute(
"UPDATE accounts SET proxy = ?, updated_at = datetime('now') WHERE id = ?",
(proxy["address"], acc_id),
)
await db.commit()
from app.core.proxy_manager import proxy_manager
await proxy_manager.load()
logger.info(f"Reassigned {len(proxies)} proxies to {len(account_ids)} accounts (round-robin)")
return {"assigned": len(account_ids), "proxies_used": len(proxies)}
@router.post("/clear-proxies")
async def clear_all_proxies():
"""Remove proxy assignment from all accounts."""
db = await get_db()
cursor = await db.execute("UPDATE accounts SET proxy = NULL, updated_at = datetime('now') WHERE proxy IS NOT NULL AND proxy != ''")
await db.commit()
count = cursor.rowcount
from app.core.proxy_manager import proxy_manager
await proxy_manager.load()
logger.info(f"Cleared proxies from {count} accounts")
return {"cleared": count}
@router.post("/import", response_model=BulkImportResult)
async def import_accounts(file: UploadFile = File(...)):
"""Import accounts.
Supported formats (: or | separator):
login|password|{mafile_json}
login|password|email|email_password|{mafile_json}
login|password|email|email_password|{mafile_json}|notes
login|password|email|email_password
login|password|email|email_password|notes (no mafile, with notes)
"""
content = (await file.read()).decode("utf-8", errors="ignore")
result = BulkImportResult()
db = await get_db()
for raw_line in content.strip().splitlines():
line = raw_line.strip()
if not line:
continue
notes: str | None = None
# Split off embedded mafile JSON — find matching braces
json_start = line.find("{")
if json_start != -1:
depth = 0
json_end = json_start
for i in range(json_start, len(line)):
if line[i] == "{":
depth += 1
elif line[i] == "}":
depth -= 1
if depth == 0:
json_end = i
break
mafile_str = line[json_start : json_end + 1]
prefix = line[:json_start].rstrip("|:")
suffix = line[json_end + 1 :].lstrip("|:")
if suffix:
notes = suffix
else:
mafile_str = None
prefix = line
sep = "|" if "|" in prefix else ":"
parts = prefix.split(sep)
if mafile_str:
# Accepted: 2 fields (login|pass) or 4 fields (login|pass|email|email_pass)
if len(parts) == 2:
login, password = parts
email = email_password = None
elif len(parts) == 4:
login, password, email, email_password = parts
else:
result.errors.append(f"Invalid prefix (need 2 or 4 fields): {line[:50]}")
result.skipped += 1
continue
try:
mafile_data = json.loads(mafile_str)
except json.JSONDecodeError as exc:
result.errors.append(f"Bad mafile JSON on line starting '{prefix[:30]}': {exc}")
result.skipped += 1
continue
# Fall back to mail fields embedded in the mafile JSON itself
if not email:
email = mafile_data.get("mail") or mafile_data.get("email") or None
email_password = mafile_data.get("mail_password") or mafile_data.get("email_password") or None
else:
# No mafile — 4 fields, or 5 fields with notes
if len(parts) == 5:
login, password, email, email_password, notes = parts
elif len(parts) == 4:
login, password, email, email_password = parts
else:
result.errors.append(f"Invalid line (need 4-5 fields): {line[:50]}")
result.skipped += 1
continue
mafile_data = None
try:
# Check if account with this login already exists
existing = await db.execute(
"SELECT id FROM accounts WHERE login = ?", (login,)
)
if await existing.fetchone():
result.errors.append(f"{login}: уже существует")
result.skipped += 1
continue
await db.execute(
"""INSERT INTO accounts (login, password, email, email_password, notes)
VALUES (?, ?, ?, ?, ?)""",
(login, password, email or None, email_password or None, notes or None),
)
result.imported += 1
except Exception as exc:
result.errors.append(f"{login}: {exc}")
result.skipped += 1
continue
if mafile_data:
account_name = mafile_data.get("account_name") or login
shared_secret = mafile_data.get("shared_secret") or ""
identity_secret = mafile_data.get("identity_secret") or ""
steam_id = (mafile_data.get("Session") or {}).get("SteamID") or None
settings.mafiles_dir.mkdir(parents=True, exist_ok=True)
safe_name = Path(account_name).name
dest = settings.mafiles_dir / f"{safe_name}.mafile"
dest.write_text(json.dumps(mafile_data, ensure_ascii=False), encoding="utf-8")
await db.execute(
"""UPDATE accounts
SET mafile_path = ?, shared_secret = ?, identity_secret = ?, steam_id = ?
WHERE login = ? AND steam_id IS NULL""",
(str(dest), shared_secret, identity_secret, steam_id, login),
)
await db.commit()
logger.info(f"Imported {result.imported} accounts, skipped {result.skipped}")
return result
@router.post("/{account_id}/mafile")
async def upload_mafile(account_id: int, file: UploadFile = File(...)):
"""Bind a .mafile to an account."""
db = await get_db()
cursor = await db.execute("SELECT * FROM accounts WHERE id = ?", (account_id,))
row = await cursor.fetchone()
if not row:
raise HTTPException(status_code=404, detail="Account not found")
content = (await file.read()).decode("utf-8")
try:
mafile = MafileData.model_validate_json(content)
except Exception as exc:
raise HTTPException(status_code=400, detail=f"Invalid mafile format: {exc}")
settings.mafiles_dir.mkdir(parents=True, exist_ok=True)
steam_id = mafile.Session.SteamID or row["steam_id"] or account_id
safe_name = Path(str(steam_id)).name
mafile_path = settings.mafiles_dir / f"{safe_name}.mafile"
mafile_path.write_text(content, encoding="utf-8")
await db.execute(
"""UPDATE accounts SET mafile_path = ?, shared_secret = ?, identity_secret = ?,
steam_id = COALESCE(steam_id, ?), updated_at = datetime('now') WHERE id = ?""",
(
str(mafile_path),
mafile.shared_secret,
mafile.identity_secret,
str(mafile.Session.SteamID) if mafile.Session.SteamID else None,
account_id,
),
)
await db.commit()
return {"status": "ok", "mafile_path": str(mafile_path)}