2026-05-26 17:29:35 +08:00
|
|
|
from collections.abc import Generator
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from typing import Annotated
|
|
|
|
|
|
|
|
|
|
from fastapi import Depends, Header, HTTPException, status
|
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
|
|
|
|
from app.db.session import get_session_factory
|
2026-07-13 14:45:36 +08:00
|
|
|
from app.services.auth import AuthService
|
|
|
|
|
from app.services.auth_sessions import AuthSessionService
|
2026-06-20 14:41:59 +08:00
|
|
|
|
|
|
|
|
|
2026-05-26 17:29:35 +08:00
|
|
|
def get_db() -> Generator[Session, None, None]:
|
|
|
|
|
db = get_session_factory()()
|
|
|
|
|
try:
|
|
|
|
|
yield db
|
|
|
|
|
finally:
|
|
|
|
|
db.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(slots=True)
|
2026-05-20 09:36:01 +08:00
|
|
|
class CurrentUserContext:
|
|
|
|
|
username: str
|
|
|
|
|
name: str
|
|
|
|
|
role_codes: list[str]
|
|
|
|
|
is_admin: bool
|
2026-07-13 11:58:48 +08:00
|
|
|
tenant_id: str = "default"
|
2026-05-20 09:36:01 +08:00
|
|
|
department_name: str = ""
|
2026-05-26 17:29:35 +08:00
|
|
|
cost_center: str = ""
|
2026-06-01 17:07:14 +08:00
|
|
|
position: str = ""
|
|
|
|
|
grade: str = ""
|
|
|
|
|
employee_no: str = ""
|
|
|
|
|
manager_name: str = ""
|
2026-07-13 14:45:36 +08:00
|
|
|
employee_id: str = ""
|
|
|
|
|
auth_session_id: str = ""
|
2026-05-26 17:29:35 +08:00
|
|
|
|
|
|
|
|
|
2026-05-11 05:18:16 +00:00
|
|
|
def get_current_user(
|
2026-07-13 14:45:36 +08:00
|
|
|
db: Annotated[Session, Depends(get_db)],
|
|
|
|
|
authorization: Annotated[
|
2026-06-01 17:07:14 +08:00
|
|
|
str | None,
|
2026-07-13 14:45:36 +08:00
|
|
|
Header(description="登录接口签发的 `Authorization: Bearer <token>`。"),
|
2026-06-01 17:07:14 +08:00
|
|
|
] = None,
|
2026-07-13 14:45:36 +08:00
|
|
|
) -> CurrentUserContext:
|
|
|
|
|
return _authenticate_bearer_user(db, authorization)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_optional_current_user(
|
|
|
|
|
db: Annotated[Session, Depends(get_db)],
|
|
|
|
|
authorization: Annotated[
|
2026-06-01 17:07:14 +08:00
|
|
|
str | None,
|
2026-07-13 14:45:36 +08:00
|
|
|
Header(description="系统已初始化时用于管理员校验的 Bearer Token。"),
|
2026-06-01 17:07:14 +08:00
|
|
|
] = None,
|
2026-07-13 14:45:36 +08:00
|
|
|
) -> CurrentUserContext | None:
|
|
|
|
|
if not str(authorization or "").strip():
|
|
|
|
|
return None
|
|
|
|
|
return _authenticate_bearer_user(db, authorization)
|
|
|
|
|
|
2026-05-26 17:29:35 +08:00
|
|
|
|
2026-07-13 14:45:36 +08:00
|
|
|
def _authenticate_bearer_user(db: Session, authorization: str | None) -> CurrentUserContext:
|
|
|
|
|
access_token = _extract_bearer_token(authorization)
|
|
|
|
|
auth_session = AuthSessionService(db).authenticate(access_token)
|
|
|
|
|
if auth_session is None:
|
|
|
|
|
raise _unauthorized("登录会话不存在、已过期或已退出,请重新登录。")
|
|
|
|
|
|
|
|
|
|
user = AuthService(db).get_session_user(auth_session)
|
|
|
|
|
if user is None:
|
|
|
|
|
raise _unauthorized("当前登录用户不存在、已停用或权限已失效。")
|
2026-05-26 17:29:35 +08:00
|
|
|
|
|
|
|
|
return CurrentUserContext(
|
2026-07-13 14:45:36 +08:00
|
|
|
username=user.username,
|
|
|
|
|
name=user.name,
|
|
|
|
|
role_codes=[_normalize_role_code(item) for item in user.role_codes],
|
|
|
|
|
is_admin=user.is_admin,
|
|
|
|
|
tenant_id=user.tenant_id,
|
|
|
|
|
department_name=user.department,
|
|
|
|
|
cost_center=user.cost_center,
|
|
|
|
|
position=user.position,
|
|
|
|
|
grade=user.grade,
|
|
|
|
|
employee_no=user.employee_no,
|
|
|
|
|
manager_name=user.manager_name,
|
|
|
|
|
employee_id=user.employee_id or "",
|
|
|
|
|
auth_session_id=auth_session.id,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_bearer_token(authorization: str | None) -> str:
|
|
|
|
|
normalized = str(authorization or "").strip()
|
|
|
|
|
scheme, separator, token = normalized.partition(" ")
|
|
|
|
|
if not separator or scheme.casefold() != "bearer" or not token.strip():
|
|
|
|
|
raise _unauthorized("请先登录后再访问。")
|
|
|
|
|
return token.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _unauthorized(detail: str) -> HTTPException:
|
|
|
|
|
return HTTPException(
|
|
|
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
|
|
|
detail=detail,
|
|
|
|
|
headers={"WWW-Authenticate": "Bearer"},
|
2026-05-20 09:36:01 +08:00
|
|
|
)
|
2026-05-26 17:29:35 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_role_code(value: str | None) -> str:
|
|
|
|
|
role_code = str(value or "").strip().lower()
|
|
|
|
|
if role_code == "auditor":
|
|
|
|
|
return "budget_monitor"
|
|
|
|
|
return role_code
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _current_user_role_codes(current_user: CurrentUserContext) -> set[str]:
|
2026-07-13 14:45:36 +08:00
|
|
|
return {
|
|
|
|
|
_normalize_role_code(item)
|
|
|
|
|
for item in current_user.role_codes
|
|
|
|
|
if _normalize_role_code(item)
|
2026-05-26 17:29:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-05-18 02:53:06 +00:00
|
|
|
def require_admin_user(
|
|
|
|
|
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
|
|
|
|
|
) -> CurrentUserContext:
|
2026-05-26 17:29:35 +08:00
|
|
|
if current_user.is_admin or "manager" in _current_user_role_codes(current_user):
|
2026-05-26 12:16:20 +08:00
|
|
|
return current_user
|
2026-05-26 17:29:35 +08:00
|
|
|
|
2026-05-18 02:53:06 +00:00
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
|
|
|
detail="只有管理员可以上传、删除或修改知识库文件。",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-05-26 12:16:20 +08:00
|
|
|
def require_platform_admin_user(
|
|
|
|
|
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
|
|
|
|
|
) -> CurrentUserContext:
|
|
|
|
|
if current_user.is_admin:
|
|
|
|
|
return current_user
|
|
|
|
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
|
|
|
detail="只有 admin 管理员可以执行该操作。",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-05-18 02:53:06 +00:00
|
|
|
def require_rule_editor_user(
|
|
|
|
|
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
|
|
|
|
|
) -> CurrentUserContext:
|
2026-05-26 17:29:35 +08:00
|
|
|
role_codes = _current_user_role_codes(current_user)
|
2026-05-18 02:53:06 +00:00
|
|
|
if current_user.is_admin or "manager" in role_codes or "finance" in role_codes:
|
|
|
|
|
return current_user
|
|
|
|
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
2026-05-26 17:29:35 +08:00
|
|
|
detail="只有财务人员或高级财务人员可以编辑规则草稿。",
|
2026-05-18 02:53:06 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def require_rule_reviewer_user(
|
|
|
|
|
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
|
|
|
|
|
) -> CurrentUserContext:
|
2026-05-26 17:29:35 +08:00
|
|
|
role_codes = _current_user_role_codes(current_user)
|
2026-05-18 02:53:06 +00:00
|
|
|
if current_user.is_admin or "manager" in role_codes:
|
|
|
|
|
return current_user
|
|
|
|
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
2026-05-26 17:29:35 +08:00
|
|
|
detail="只有高级财务人员或 admin 管理员可以执行该操作。",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def require_budget_viewer_user(
|
|
|
|
|
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
|
|
|
|
|
) -> CurrentUserContext:
|
|
|
|
|
role_codes = _current_user_role_codes(current_user)
|
|
|
|
|
if current_user.is_admin or role_codes & {"budget_monitor", "executive"}:
|
|
|
|
|
return current_user
|
|
|
|
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
|
|
|
detail="只有预算监控员或高级财务人员可以查看预算中心。",
|
2026-05-18 02:53:06 +00:00
|
|
|
)
|
2026-05-26 17:29:35 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def require_budget_editor_user(
|
|
|
|
|
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
|
|
|
|
|
) -> CurrentUserContext:
|
|
|
|
|
role_codes = _current_user_role_codes(current_user)
|
|
|
|
|
if current_user.is_admin or "executive" in role_codes:
|
|
|
|
|
return current_user
|
|
|
|
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
|
|
|
detail="只有 admin 管理员或高级财务人员可以维护预算额度。",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_budget_scope_limited_user(current_user: CurrentUserContext) -> bool:
|
|
|
|
|
if current_user.is_admin:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
role_codes = _current_user_role_codes(current_user)
|
|
|
|
|
return "budget_monitor" in role_codes and "executive" not in role_codes
|