FEAT: thêm nút bypass cho track strip để bypass không qua mastering panel
This commit is contained in:
+79
-14
@@ -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)):
|
||||
|
||||
Reference in New Issue
Block a user