feat: multi-admin support — named logins instead of one shared password
Matches Marzban's multi-admin (WIP there) and closes a real gap vs both. New 'admins' table (username + PBKDF2-SHA256 password hash, 200k iterations, random salt per account, stdlib hashlib/hmac only — no new dependency), admin_sessions now tracks which admin is logged in. Existing installs aren't broken: on first run, if no admins exist yet, a default 'admin' account is seeded from the current ADMIN_PANEL_PASSWORD — old password keeps working under username 'admin', pre-filled on the login screen. Admin management lives in Settings: list, add (username + password, min 8 chars), remove. Can't delete the last remaining admin or your own currently-logged-in account. Sidebar now shows who's logged in. Verified end-to-end: bootstrap, correct/wrong/nonexistent login, session->admin resolution, last-admin-delete protection, duplicate username rejection, add/remove round trip, and that identical passwords hash to different values (unique salt) but both verify. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
81cbc4e391
commit
9e6e314c94
3 changed files with 210 additions and 13 deletions
50
api.py
50
api.py
|
|
@ -18,7 +18,7 @@ import payments
|
|||
import xray_manager
|
||||
from config import (
|
||||
PLANS, PLANS_BY_CODE, SITE_DOMAIN, SUB_DOMAIN, PANEL_DOMAIN,
|
||||
ADMIN_PANEL_PASSWORD, BOT_USERNAME, BOT_TOKEN, BASE_DIR,
|
||||
BOT_USERNAME, BOT_TOKEN, BASE_DIR,
|
||||
HWID_LIMIT_ENABLED, HWID_FALLBACK_LIMIT,
|
||||
)
|
||||
|
||||
|
|
@ -437,9 +437,12 @@ async def register_node(token: str, request: Request):
|
|||
|
||||
@app.post("/admin/api/login")
|
||||
def admin_login(response: Response, body: dict = Body(...)):
|
||||
if body.get("password") != ADMIN_PANEL_PASSWORD:
|
||||
raise HTTPException(401, "wrong password")
|
||||
token = db.create_admin_session()
|
||||
username = (body.get("username") or "").strip()
|
||||
password = body.get("password") or ""
|
||||
admin = db.verify_admin_login(username, password)
|
||||
if not admin:
|
||||
raise HTTPException(401, "wrong username or password")
|
||||
token = db.create_admin_session(admin["id"])
|
||||
response.set_cookie(ADMIN_COOKIE, token, httponly=True, secure=True, samesite="strict", max_age=7 * 24 * 3600)
|
||||
return {"ok": True}
|
||||
|
||||
|
|
@ -456,7 +459,44 @@ def admin_logout(request: Request, response: Response):
|
|||
@app.get("/admin/api/me")
|
||||
def admin_me(request: Request):
|
||||
token = request.cookies.get(ADMIN_COOKIE)
|
||||
return {"authenticated": db.validate_admin_session(token)}
|
||||
admin = db.get_session_admin(token)
|
||||
return {"authenticated": admin is not None, "username": admin["username"] if admin else None}
|
||||
|
||||
|
||||
@app.get("/admin/api/admins")
|
||||
def admin_list_admins(request: Request):
|
||||
require_admin(request)
|
||||
return db.list_admins()
|
||||
|
||||
|
||||
@app.post("/admin/api/admins")
|
||||
def admin_create_admin(request: Request, body: dict = Body(...)):
|
||||
require_admin(request)
|
||||
username = (body.get("username") or "").strip()
|
||||
password = body.get("password") or ""
|
||||
if len(username) < 3:
|
||||
raise HTTPException(400, "username too short")
|
||||
if len(password) < 8:
|
||||
raise HTTPException(400, "password too short")
|
||||
try:
|
||||
return db.create_admin(username, password)
|
||||
except ValueError as e:
|
||||
raise HTTPException(400, str(e))
|
||||
|
||||
|
||||
@app.delete("/admin/api/admins/{admin_id}")
|
||||
def admin_delete_admin(admin_id: int, request: Request):
|
||||
token = request.cookies.get(ADMIN_COOKIE)
|
||||
current = db.get_session_admin(token)
|
||||
if not current:
|
||||
raise HTTPException(401, "unauthorized")
|
||||
if current["id"] == admin_id:
|
||||
raise HTTPException(400, "cannot delete your own account while logged in as it")
|
||||
try:
|
||||
db.delete_admin(admin_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(400, str(e))
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue