FEAT: thêm nút bypass cho track strip để bypass không qua mastering panel

This commit is contained in:
2026-08-03 15:47:10 +07:00
parent 6f55d36085
commit a9da813cb1
28 changed files with 2076 additions and 233 deletions
+79 -14
View File
@@ -1,10 +1,15 @@
import uuid
import time
from fastapi import APIRouter, HTTPException, Header, Depends
import threading
from fastapi import APIRouter, HTTPException, Header, Depends, Request, Response
from fastapi.responses import JSONResponse
from pydantic import BaseModel, EmailStr
from typing import Optional
from app.models.user import get_db_connection
from app.core.auth import hash_password, verify_password, create_token, decode_token, seed_admin
from app.core.auth import (
hash_password, verify_password, create_token, decode_token, seed_admin,
COOKIE_NAME, X_AUTH_HEADER,
)
router = APIRouter()
@@ -21,10 +26,49 @@ class ChangePasswordRequest(BaseModel):
old_password: str
new_password: str
def get_current_user(authorization: Optional[str] = Header(None)):
if not authorization or not authorization.startswith("Bearer "):
# ── Brute-force guard: in-memory per-IP failed-login limiter ──
_LOGIN_FAILURES = {} # ip -> [timestamps]
_LOGIN_LOCK = threading.Lock()
MAX_LOGIN_ATTEMPTS = 10
LOGIN_WINDOW_SEC = 900 # 15 min
LOGIN_BLOCK_SEC = 900
def _check_login_ratelimit(ip: str):
now = time.time()
with _LOGIN_LOCK:
stamps = [t for t in _LOGIN_FAILURES.get(ip, []) if now - t < LOGIN_WINDOW_SEC]
if len(stamps) >= MAX_LOGIN_ATTEMPTS:
raise HTTPException(status_code=429, detail="Quá nhiều lần đăng nhập thất bại. Vui lòng thử lại sau 15 phút.")
_LOGIN_FAILURES[ip] = stamps
def _record_login_failure(ip: str):
now = time.time()
with _LOGIN_LOCK:
stamps = _LOGIN_FAILURES.setdefault(ip, [])
stamps.append(now)
_LOGIN_FAILURES[ip] = [t for t in stamps if now - t < LOGIN_WINDOW_SEC]
def _record_login_success(ip: str):
with _LOGIN_LOCK:
_LOGIN_FAILURES.pop(ip, None)
def _set_auth_cookie(response: Response, token: str):
response.set_cookie(
COOKIE_NAME, token,
max_age=7 * 24 * 3600, httponly=True, samesite="lax",
# path="/" (default); secure flag set by proxy when behind TLS
)
def get_current_user(request: Request, authorization: Optional[str] = Header(None), x_auth_token: Optional[str] = Header(None)):
token = None
if authorization and authorization.startswith("Bearer "):
token = authorization.split(" ")[1]
elif x_auth_token:
token = x_auth_token
elif request.cookies.get(COOKIE_NAME):
token = request.cookies.get(COOKIE_NAME)
if not token:
raise HTTPException(status_code=401, detail="Thiếu Token xác thực hoặc Token không hợp lệ")
token = authorization.split(" ")[1]
payload = decode_token(token)
if not payload:
raise HTTPException(status_code=401, detail="Token đã hết hạn hoặc không hợp lệ")
@@ -39,7 +83,10 @@ def enforce_password_changed(user: dict):
)
@router.post("/login")
async def login(req: LoginRequest):
async def login(req: LoginRequest, request: Request):
client_ip = request.client.host if request.client else "unknown"
_check_login_ratelimit(client_ip)
conn = get_db_connection()
cursor = conn.cursor()
@@ -65,14 +112,17 @@ async def login(req: LoginRequest):
conn.close()
if not user or not user["is_active"]:
_record_login_failure(client_ip)
raise HTTPException(status_code=400, detail="Tài khoản hoặc mật khẩu không chính xác")
if not verify_password(password, user["hashed_password"]):
_record_login_failure(client_ip)
raise HTTPException(status_code=400, detail="Tài khoản hoặc mật khẩu không chính xác")
token = create_token(user["id"], user["username"], user["role"], user["must_change_password"])
return {
_record_login_success(client_ip)
resp = JSONResponse({
"access_token": token,
"user": {
"id": user["id"],
@@ -81,13 +131,24 @@ async def login(req: LoginRequest):
"role": user["role"],
"must_change_password": bool(user["must_change_password"])
}
}
})
_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:
raise HTTPException(status_code=400, detail="Mật khẩu phải có ít nhất 8 ký tự")
lowered = password.lower()
if lowered in ("admin123", "password", "12345678", "123456789", "qwerty123"):
raise HTTPException(status_code=400, detail="Mật khẩu quá dễ đoán, vui lòng chọn mật khẩu khác")
@router.post("/register")
async def register(req: RegisterRequest):
async def register(req: RegisterRequest, request: Request):
username = req.username.strip()
email = req.email.strip()
password = req.password.strip()
_validate_password_strength(password)
conn = get_db_connection()
cursor = conn.cursor()
@@ -115,7 +176,7 @@ async def register(req: RegisterRequest):
conn.close()
token = create_token(user_id, username, "standard", False)
return {
resp = JSONResponse({
"access_token": token,
"user": {
"id": user_id,
@@ -124,7 +185,9 @@ async def register(req: RegisterRequest):
"role": "standard",
"must_change_password": False
}
}
})
_set_auth_cookie(resp, token)
return resp
@router.post("/change-password")
async def change_password(req: ChangePasswordRequest, current_user: dict = Depends(get_current_user)):
@@ -153,10 +216,12 @@ async def change_password(req: ChangePasswordRequest, current_user: dict = Depen
conn.close()
new_token = create_token(updated_user["id"], updated_user["username"], updated_user["role"], False)
return {
resp = JSONResponse({
"message": "Đổi mật khẩu thành công!",
"access_token": new_token
}
})
_set_auth_cookie(resp, new_token)
return resp
@router.get("/profile")
async def get_profile(current_user: dict = Depends(get_current_user)):