feat(audit): record login events and trigger users

This commit is contained in:
wren
2026-05-09 20:07:44 +08:00
parent 4d56652879
commit e8a93f25a6
5 changed files with 152 additions and 4 deletions
@@ -1,5 +1,10 @@
"""评查控制器。""" """评查控制器。"""
from typing import Any
from fastapi import Depends
from fastapi_common.fastapi_common_security.security import verify_access_token
from fastapi_common.fastapi_common_web.controller import BaseController from fastapi_common.fastapi_common_web.controller import BaseController
from fastapi_common.fastapi_common_web.domain.responses import Result from fastapi_common.fastapi_common_web.domain.responses import Result
@@ -17,7 +22,7 @@ class AuditController(BaseController):
self.AuditService: IAuditService = AuditServiceImpl() self.AuditService: IAuditService = AuditServiceImpl()
@self.router.post("/run", response_model=Result[AuditRunVO]) @self.router.post("/run", response_model=Result[AuditRunVO])
async def RunAudit(body: AuditRunDTO): async def RunAudit(body: AuditRunDTO, payload: dict[str, Any] = Depends(verify_access_token)):
"""触发文档评查 """触发文档评查
对指定文档执行 LeAudit 完整评查链路。 对指定文档执行 LeAudit 完整评查链路。
@@ -27,6 +32,7 @@ class AuditController(BaseController):
RuleType=body.ruleType, RuleType=body.ruleType,
Force=body.force, Force=body.force,
Speed=body.speed, Speed=body.speed,
TriggerUserId=int(payload["user_id"]),
) )
return Result.success(data=run) return Result.success(data=run)
@@ -22,6 +22,7 @@ from fastapi_common.fastapi_common_security.security import verify_access_token
from fastapi_modules.fastapi_leaudit.domian.Dto.auth.loginDto import PasswordLoginDTO from fastapi_modules.fastapi_leaudit.domian.Dto.auth.loginDto import PasswordLoginDTO
from fastapi_modules.fastapi_leaudit.services import IAuthService from fastapi_modules.fastapi_leaudit.services import IAuthService
from fastapi_modules.fastapi_leaudit.services.impl.authServiceImpl import AuthServiceImpl from fastapi_modules.fastapi_leaudit.services.impl.authServiceImpl import AuthServiceImpl
from fastapi_modules.fastapi_leaudit.services.impl.usageStatsServiceImpl import UsageStatsServiceImpl
class AuthController(BaseController): class AuthController(BaseController):
@@ -30,6 +31,44 @@ class AuthController(BaseController):
def __init__(self): def __init__(self):
super().__init__(prefix="/auth", tags=["认证"]) super().__init__(prefix="/auth", tags=["认证"])
self.AuthService: IAuthService = AuthServiceImpl() self.AuthService: IAuthService = AuthServiceImpl()
self.UsageStatsService = UsageStatsServiceImpl()
def _extract_client_ip(request_obj: Request) -> str | None:
forwarded_for = request_obj.headers.get("x-forwarded-for", "").strip()
if forwarded_for:
return forwarded_for.split(",")[0].strip() or None
real_ip = request_obj.headers.get("x-real-ip", "").strip()
if real_ip:
return real_ip or None
if request_obj.client and request_obj.client.host:
return request_obj.client.host
return None
def _extract_user_agent(request_obj: Request) -> str | None:
user_agent = request_obj.headers.get("user-agent", "").strip()
return user_agent or None
async def _record_login_event(
*,
request_obj: Request,
user_info: dict[str, Any] | None,
sub: str | None,
login_result: str,
login_type: str,
failure_reason: str | None = None,
) -> None:
try:
await self.UsageStatsService.RecordLoginEvent(
UserInfo=user_info,
Sub=sub,
LoginResult=login_result,
LoginType=login_type,
IpAddress=_extract_client_ip(request_obj),
UserAgent=_extract_user_agent(request_obj),
FailureReason=failure_reason,
)
except Exception as record_error:
logger.warning("记录登录审计事件失败: %s", record_error)
@self.router.post("/login") @self.router.post("/login")
async def Login(RequestObj: Request): async def Login(RequestObj: Request):
@@ -41,12 +80,16 @@ class AuthController(BaseController):
""" """
try: try:
requestData = await RequestObj.json() requestData = await RequestObj.json()
sub: str | None = None
login_type = "password"
if "userInfo" in requestData and isinstance(requestData["userInfo"], dict) and "sub" in requestData["userInfo"]: if "userInfo" in requestData and isinstance(requestData["userInfo"], dict) and "sub" in requestData["userInfo"]:
logger.info("检测到 OAuth 登录请求") logger.info("检测到 OAuth 登录请求")
ui = requestData["userInfo"] ui = requestData["userInfo"]
sub = ui["sub"]
login_type = "oauth"
vo = await self.AuthService.OAuthLogin( vo = await self.AuthService.OAuthLogin(
Sub=ui["sub"], Sub=sub,
Username=ui.get("username"), Username=ui.get("username"),
Nickname=ui.get("nickname"), Nickname=ui.get("nickname"),
Email=ui.get("email"), Email=ui.get("email"),
@@ -59,16 +102,32 @@ class AuthController(BaseController):
) )
elif "username" in requestData and "password" in requestData: elif "username" in requestData and "password" in requestData:
logger.info(f"检测到密码登录请求 - username={requestData['username']}") logger.info(f"检测到密码登录请求 - username={requestData['username']}")
sub = str(requestData["username"])
vo = await self.AuthService.PasswordLogin( vo = await self.AuthService.PasswordLogin(
Sub=requestData["username"], Sub=sub,
Password=requestData["password"], Password=requestData["password"],
) )
else: else:
await _record_login_event(
request_obj=RequestObj,
user_info=None,
sub=None,
login_result="failed",
login_type="unknown",
failure_reason="invalid_request",
)
return JSONResponse( return JSONResponse(
status_code=400, status_code=400,
content={"success": False, "message": "无效的登录请求格式", "data": None}, content={"success": False, "message": "无效的登录请求格式", "data": None},
) )
await _record_login_event(
request_obj=RequestObj,
user_info=vo.user_info if isinstance(vo.user_info, dict) else None,
sub=sub,
login_result="success",
login_type=login_type,
)
return JSONResponse(status_code=200, content={ return JSONResponse(status_code=200, content={
"success": True, "success": True,
"message": "ok", "message": "ok",
@@ -83,12 +142,56 @@ class AuthController(BaseController):
except LeauditException as e: except LeauditException as e:
logger.error(f"登录失败: {e.message}") logger.error(f"登录失败: {e.message}")
request_data = {}
try:
request_data = await RequestObj.json()
except Exception:
request_data = {}
request_user_info = request_data.get("userInfo") if isinstance(request_data.get("userInfo"), dict) else None
request_sub = None
login_type = "unknown"
if request_user_info and request_user_info.get("sub"):
request_sub = str(request_user_info["sub"])
login_type = "oauth"
elif request_data.get("username"):
request_sub = str(request_data["username"])
login_type = "password"
await _record_login_event(
request_obj=RequestObj,
user_info=None,
sub=request_sub,
login_result="failed",
login_type=login_type,
failure_reason=e.message,
)
return JSONResponse( return JSONResponse(
status_code=e.status.value, status_code=e.status.value,
content={"success": False, "message": e.message, "data": None}, content={"success": False, "message": e.message, "data": None},
) )
except Exception as e: except Exception as e:
logger.error(f"登录失败: {e}") logger.error(f"登录失败: {e}")
request_data = {}
try:
request_data = await RequestObj.json()
except Exception:
request_data = {}
request_user_info = request_data.get("userInfo") if isinstance(request_data.get("userInfo"), dict) else None
request_sub = None
login_type = "unknown"
if request_user_info and request_user_info.get("sub"):
request_sub = str(request_user_info["sub"])
login_type = "oauth"
elif request_data.get("username"):
request_sub = str(request_data["username"])
login_type = "password"
await _record_login_event(
request_obj=RequestObj,
user_info=None,
sub=request_sub,
login_result="failed",
login_type=login_type,
failure_reason=str(e),
)
return JSONResponse(status_code=401, content={ return JSONResponse(status_code=401, content={
"success": False, "message": "登录失败,请稍后重试", "data": None, "success": False, "message": "登录失败,请稍后重试", "data": None,
}) })
@@ -100,17 +203,52 @@ class AuthController(BaseController):
requestData = await RequestObj.json() requestData = await RequestObj.json()
dto = PasswordLoginDTO(**requestData) dto = PasswordLoginDTO(**requestData)
vo = await self.AuthService.PasswordLogin(Sub=dto.sub, Password=dto.password) vo = await self.AuthService.PasswordLogin(Sub=dto.sub, Password=dto.password)
await _record_login_event(
request_obj=RequestObj,
user_info=vo.user_info if isinstance(vo.user_info, dict) else None,
sub=dto.sub,
login_result="success",
login_type="password",
)
return JSONResponse(status_code=200, content={ return JSONResponse(status_code=200, content={
"success": True, "message": "ok", "data": vo.model_dump(), "success": True, "message": "ok", "data": vo.model_dump(),
}) })
except LeauditException as e: except LeauditException as e:
logger.error(f"密码登录失败: {e.message}") logger.error(f"密码登录失败: {e.message}")
request_sub = None
try:
request_data = await RequestObj.json()
request_sub = request_data.get("sub") or request_data.get("username")
except Exception:
request_sub = None
await _record_login_event(
request_obj=RequestObj,
user_info=None,
sub=str(request_sub) if request_sub else None,
login_result="failed",
login_type="password",
failure_reason=e.message,
)
return JSONResponse( return JSONResponse(
status_code=e.status.value, status_code=e.status.value,
content={"success": False, "message": e.message, "data": None}, content={"success": False, "message": e.message, "data": None},
) )
except Exception as e: except Exception as e:
logger.error(f"密码登录失败: {e}") logger.error(f"密码登录失败: {e}")
request_sub = None
try:
request_data = await RequestObj.json()
request_sub = request_data.get("sub") or request_data.get("username")
except Exception:
request_sub = None
await _record_login_event(
request_obj=RequestObj,
user_info=None,
sub=str(request_sub) if request_sub else None,
login_result="failed",
login_type="password",
failure_reason=str(e),
)
return JSONResponse(status_code=401, content={ return JSONResponse(status_code=401, content={
"success": False, "message": "登录失败,请稍后重试", "data": None, "success": False, "message": "登录失败,请稍后重试", "data": None,
}) })
@@ -15,6 +15,7 @@ class IAuditService(ABC):
RuleType: str | None = None, RuleType: str | None = None,
Force: bool = False, Force: bool = False,
Speed: str = "normal", Speed: str = "normal",
TriggerUserId: int | None = None,
) -> AuditRunVO: ) -> AuditRunVO:
"""触发文档评查。""" """触发文档评查。"""
... ...
@@ -111,13 +111,14 @@ class AuditServiceImpl(IAuditService):
RuleType: str | None = None, RuleType: str | None = None,
Force: bool = False, Force: bool = False,
Speed: str = "normal", Speed: str = "normal",
TriggerUserId: int | None = None,
) -> AuditRunVO: ) -> AuditRunVO:
"""触发文档评查。 """触发文档评查。
当前阶段只负责创建 run 并投递 worker,不在 HTTP 请求内同步执行。 当前阶段只负责创建 run 并投递 worker,不在 HTTP 请求内同步执行。
""" """
async with GetAsyncSession() as session: async with GetAsyncSession() as session:
logger.info(f"触发评查: documentId={DocumentId}, ruleType={RuleType}") logger.info(f"触发评查: documentId={DocumentId}, ruleType={RuleType}, triggerUserId={TriggerUserId}")
normalizedSpeed = _normalize_speed(Speed) normalizedSpeed = _normalize_speed(Speed)
await session.execute( await session.execute(
text( text(
@@ -205,6 +206,7 @@ class AuditServiceImpl(IAuditService):
documentFileId=documentFile.Id, documentFileId=documentFile.Id,
runNo=int(latestRunNo) + 1, runNo=int(latestRunNo) + 1,
triggerSource=triggerSource, triggerSource=triggerSource,
triggerUserId=TriggerUserId,
status="queued", status="queued",
phase="dispatch", phase="dispatch",
ruleSetId=int(binding["rule_set_id"]), ruleSetId=int(binding["rule_set_id"]),
@@ -261,6 +261,7 @@ class DocumentServiceImpl(IDocumentService):
DocumentId=document.Id, DocumentId=document.Id,
Speed=Speed, Speed=Speed,
Force=duplicateUpload, Force=duplicateUpload,
TriggerUserId=CreatedBy,
) )
processingStatus = "running" if run.status in {"pending", "running"} else run.status processingStatus = "running" if run.status in {"pending", "running"} else run.status