feat(监控): 完善系统监控、登录状态与访问审计能力
This commit is contained in:
@@ -9,7 +9,8 @@ import uuid
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.login_crypto import create_login_challenge, decrypt_login_payload, get_public_key_pem
|
||||
from app.core.security import create_access_token, decode_token_allow_expired, oauth2_scheme, verify_password
|
||||
from app.core.request_context import resolve_ctms_client_type
|
||||
from app.core.security import create_access_token, decode_token, decode_token_allow_expired, oauth2_scheme, verify_password
|
||||
from app.core.deps import get_current_user, get_db_session
|
||||
from app.crud import user as user_crud
|
||||
from app.models.user import UserStatus
|
||||
@@ -26,6 +27,12 @@ from app.schemas.email_settings import (
|
||||
)
|
||||
from app.schemas.user import Token, UserRead, UserRegisterRequest, UserSelfUpdate, UserUpdate
|
||||
from app.services import email_service
|
||||
from app.services.user_login_sessions import (
|
||||
create_login_session,
|
||||
end_login_session,
|
||||
session_id_from_payload,
|
||||
touch_login_session,
|
||||
)
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
|
||||
@@ -95,7 +102,7 @@ def get_session_policy_for_client_type(client_type: str) -> SessionPolicy:
|
||||
|
||||
|
||||
def get_request_session_client_type(request: Request) -> str:
|
||||
return normalize_session_client_type(request.headers.get("x-ctms-client-type"))
|
||||
return normalize_session_client_type(resolve_ctms_client_type(request.headers))
|
||||
|
||||
|
||||
def policy_expires_at(issued_at: datetime, session_start: datetime, policy: SessionPolicy) -> datetime:
|
||||
@@ -104,8 +111,9 @@ def policy_expires_at(issued_at: datetime, session_start: datetime, policy: Sess
|
||||
return min(access_expires_at, session_expires_at)
|
||||
|
||||
|
||||
def issue_user_token(db_user, request: Request) -> Token:
|
||||
async def issue_user_token(db_user, request: Request, db: AsyncSession) -> Token:
|
||||
session_start = datetime.now(timezone.utc)
|
||||
session_id = uuid.uuid4()
|
||||
client_type = get_request_session_client_type(request)
|
||||
policy = get_session_policy_for_client_type(client_type)
|
||||
access_token = create_access_token(
|
||||
@@ -115,6 +123,14 @@ def issue_user_token(db_user, request: Request) -> Token:
|
||||
max_age_seconds=policy.absolute_max_seconds,
|
||||
issued_at=session_start,
|
||||
client_type=client_type,
|
||||
session_id=str(session_id),
|
||||
)
|
||||
await create_login_session(
|
||||
db,
|
||||
session_id=session_id,
|
||||
user_id=db_user.id,
|
||||
request=request,
|
||||
login_at=session_start,
|
||||
)
|
||||
return Token(access_token=access_token, token_type="bearer")
|
||||
|
||||
@@ -284,7 +300,7 @@ async def login_for_access_token(
|
||||
db_user = await authenticate_encrypted_password(payload, db)
|
||||
ensure_user_active(db_user)
|
||||
|
||||
return issue_user_token(db_user, request)
|
||||
return await issue_user_token(db_user, request, db)
|
||||
|
||||
|
||||
@router.post("/dev-login", response_model=Token)
|
||||
@@ -295,7 +311,7 @@ async def dev_login_for_access_token(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
|
||||
db_user = await authenticate_plain_password(payload, db)
|
||||
ensure_user_active(db_user)
|
||||
return issue_user_token(db_user, request)
|
||||
return await issue_user_token(db_user, request, db)
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserRead)
|
||||
@@ -339,11 +355,47 @@ async def extend_access_token(
|
||||
max_age_seconds=policy.absolute_max_seconds,
|
||||
issued_at=issued_at,
|
||||
client_type=normalize_session_client_type(payload.get("client_type")),
|
||||
session_id=str(session_id_from_payload(payload)),
|
||||
)
|
||||
expires_at = policy_expires_at(issued_at, session_start, policy)
|
||||
return ExtendResponse(accessToken=new_token, expiresAt=expires_at)
|
||||
|
||||
|
||||
@router.post("/session/heartbeat")
|
||||
async def heartbeat_login_session(
|
||||
request: Request,
|
||||
token: str = Depends(oauth2_scheme),
|
||||
current_user=Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
session = await touch_login_session(
|
||||
db,
|
||||
user_id=current_user.id,
|
||||
payload=decode_token(token),
|
||||
request=request,
|
||||
)
|
||||
if session is None:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="登录会话已结束")
|
||||
return {
|
||||
"status": "online",
|
||||
"last_seen_at": session.last_seen_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/session/logout", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def logout_login_session(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
current_user=Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> Response:
|
||||
await end_login_session(
|
||||
db,
|
||||
user_id=current_user.id,
|
||||
payload=decode_token(token),
|
||||
)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@router.patch("/me", response_model=UserRead)
|
||||
async def update_me(
|
||||
payload: UserSelfUpdate,
|
||||
|
||||
Reference in New Issue
Block a user