feat(rt+auth): batched input publish boi so 4 (het crackle) + auto-login loopback khong can mat khau

- fx_realtime: write_input gom FXRT_IN_SLOTS(4) frame vao _in_pending, flush
  ghi 1 lan + h.in_write += n (publish atomic -> bridge take=4 luon, het deficit)
- plugins pump: flush_input_stale(0.004) dau vong lap -> input le khong ket
- auth: _is_loopback(127.*/::1/localhost) -> get_current_user khong token = admin
  tu DB (must_change_password=False); POST /auth/local auto-login loopback, LAN 403
- ws fx-realtime: loopback khong token cho phep, remote thieu token -> close 4401
- UI: checkAuthStatus khong savedToken -> thu localLogin, thanh cong vao DAW,
  that bai -> modal login nhu cu; cache-buster v=202608212400
This commit is contained in:
2026-08-23 12:00:05 +07:00
parent 0acc3ee7ee
commit 9bb9a234b0
7 changed files with 126 additions and 23 deletions
+44
View File
@@ -52,6 +52,30 @@ def _record_login_success(ip: str):
with _LOGIN_LOCK:
_LOGIN_FAILURES.pop(ip, None)
def _is_loopback(host: Optional[str]) -> bool:
"""Standalone: request từ chính máy (127.0.0.1/::1/localhost) → bỏ qua
đăng nhập. Client LAN (IP khác) → vẫn cần mật khẩu."""
if not host:
return False
host = host.lower()
return host in ("127.0.0.1", "::1", "localhost") or host.startswith("127.")
def _loopback_admin_user() -> dict:
"""Identity admin cho loopback — real user_id từ DB; không bắt buộc đổi
mật khẩu (standalone tự vào thẳng DAW)."""
try:
conn = get_db_connection()
try:
cur = conn.execute("SELECT id, username, role FROM users WHERE username = ?", ("admin",))
row = cur.fetchone()
finally:
conn.close()
if row:
return {"user_id": row["id"], "username": row["username"], "role": row["role"], "must_change_password": False}
except Exception:
pass
return {"user_id": "local", "username": "admin", "role": "admin", "must_change_password": False}
def _set_auth_cookie(response: Response, token: str):
response.set_cookie(
COOKIE_NAME, token,
@@ -68,6 +92,10 @@ def get_current_user(request: Request, authorization: Optional[str] = Header(Non
elif request.cookies.get(COOKIE_NAME):
token = request.cookies.get(COOKIE_NAME)
if not token:
# Standalone (loopback): không cần mật khẩu — auto admin. LAN vẫn 401.
host = request.client.host if request.client else None
if _is_loopback(host):
return _loopback_admin_user()
raise HTTPException(status_code=401, detail="Thiếu Token xác thực hoặc Token không hợp lệ")
payload = decode_token(token)
if not payload:
@@ -135,6 +163,22 @@ async def login(req: LoginRequest, request: Request):
_set_auth_cookie(resp, token)
return resp
@router.post("/local")
async def local_login(request: Request, response: Response):
"""Auto-login standalone: chỉ cho phép từ loopback (máy chạy engine).
Client LAN nhận 403 → buộc đăng nhập mật khẩu qua /login."""
host = request.client.host if request.client else None
if not _is_loopback(host):
raise HTTPException(status_code=403, detail="Truy cập từ xa — cần đăng nhập")
u = _loopback_admin_user()
token = create_token(u["user_id"], u["username"], u["role"], False)
resp = JSONResponse({
"access_token": token,
"user": {"id": u["user_id"], "username": u["username"], "role": u["role"], "must_change_password": False},
})
_set_auth_cookie(resp, token)
return resp
def _validate_password_strength(password: str):
"""Minimal strength policy: >= 8 chars and not trivially common."""
if len(password) < 8: