feat(audit): record login events and trigger users
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user