203 lines
8.9 KiB
Python
203 lines
8.9 KiB
Python
import json
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
from sqlalchemy.orm import Session
|
|
|
|
from backend.app.core.config import get_settings
|
|
from backend.app.core.error_codes import ErrorCode
|
|
from backend.app.core.exceptions import AppException
|
|
from backend.app.core.security import create_access_token, decode_access_token, verify_password
|
|
from backend.app.repositories.system_repository import SystemRepository
|
|
from backend.app.services.demo_store import demo_store
|
|
|
|
|
|
class AuthService:
|
|
DEFAULT_ROLE_MENUS = {
|
|
"salesman": [{"menu_name": "我的订单", "menu_path": "/orders"}],
|
|
"manager": [{"menu_name": "审批中心", "menu_path": "/pages/approve-list"}],
|
|
"driver": [{"menu_name": "我的任务", "menu_path": "/pages/task-list"}],
|
|
"admin": [{"menu_name": "后台首页", "menu_path": "/dashboard"}],
|
|
}
|
|
|
|
DEFAULT_ROLE_PERMISSIONS = {
|
|
"salesman": ["order:create", "order:list", "order:submit", "customer:list"],
|
|
"manager": ["order:list", "order:approve", "order:cancel-approve", "logistics:task:create"],
|
|
"driver": ["driver:task:list", "driver:task:accept", "driver:task:pickup", "driver:task:deliver"],
|
|
"admin": ["system:user:list", "system:role:list", "system:menu:list", "order:list"],
|
|
}
|
|
|
|
def __init__(self) -> None:
|
|
self.repository = SystemRepository()
|
|
self.settings = get_settings()
|
|
self.token_store_path = Path(self.settings.auth_token_store_path)
|
|
self.token_store_path.parent.mkdir(parents=True, exist_ok=True)
|
|
self.revoked_tokens = self._load_revoked_tokens()
|
|
|
|
def login(self, username: str, password: str, role_type: str, session: Session | None = None) -> dict | None:
|
|
if session is not None:
|
|
try:
|
|
user = self.repository.get_user_by_username(session, username)
|
|
if user is None or not verify_password(password, user.password_hash):
|
|
raise AppException(
|
|
code=ErrorCode.UNAUTHORIZED,
|
|
message="账号不存在或密码错误",
|
|
status_code=401,
|
|
)
|
|
|
|
role = self.repository.get_role(session, user.role_id)
|
|
if role is None:
|
|
raise AppException(code=ErrorCode.NOT_FOUND, message="角色不存在", status_code=404)
|
|
if user.status != 1:
|
|
raise AppException(code=ErrorCode.FORBIDDEN, message="账号已停用", status_code=403)
|
|
if role.status != 1 or role.role_code != role_type:
|
|
raise AppException(code=ErrorCode.FORBIDDEN, message="登录角色与账号不匹配", status_code=403)
|
|
|
|
return self._build_profile(session, user, role, include_token=True)
|
|
except AppException:
|
|
raise
|
|
except SQLAlchemyError:
|
|
pass
|
|
|
|
return demo_store.login(username, role_type)
|
|
|
|
def get_me(self, token: str, session: Session | None = None) -> dict | None:
|
|
if session is not None:
|
|
try:
|
|
if self._is_token_revoked(token):
|
|
return None
|
|
payload = decode_access_token(token)
|
|
if payload is None:
|
|
return None
|
|
user_id = int(payload["user_id"])
|
|
role_code = str(payload["role_code"])
|
|
user = self.repository.get_user(session, user_id)
|
|
if user is None or user.status != 1:
|
|
return None
|
|
role = self.repository.get_role(session, user.role_id)
|
|
if role is None or role.status != 1 or role.role_code != role_code:
|
|
return None
|
|
return self._build_profile(session, user, role, include_token=False)
|
|
except (SQLAlchemyError, ValueError, KeyError, TypeError):
|
|
pass
|
|
|
|
return demo_store.get_me(token)
|
|
|
|
def logout(self, token: str) -> dict:
|
|
if token:
|
|
# token 失效需要跨进程持久化,避免服务重启后已退出的 token 又重新可用。
|
|
self._revoke_token(token)
|
|
return {"success": True}
|
|
|
|
def _build_profile(self, session: Session, user, role, include_token: bool) -> dict:
|
|
menus = self._load_role_menus(session, role.role_code)
|
|
permissions = self._load_role_permissions(session, role.role_code)
|
|
return {
|
|
"user_id": user.id,
|
|
"username": user.username,
|
|
"real_name": user.real_name,
|
|
"mobile": user.mobile,
|
|
"role_id": role.id,
|
|
"role_name": role.role_name,
|
|
"role_code": role.role_code,
|
|
"token": create_access_token({"user_id": user.id, "role_code": role.role_code}) if include_token else None,
|
|
"menus": menus,
|
|
"permissions": permissions,
|
|
}
|
|
|
|
def _load_role_menus(self, session: Session, role_code: str) -> list[dict]:
|
|
menus = self.repository.list_menus(session, {"status": 1})
|
|
if not menus:
|
|
return self.DEFAULT_ROLE_MENUS.get(role_code, [])
|
|
|
|
if role_code == "admin":
|
|
return [{"menu_name": menu.menu_name, "menu_path": menu.menu_path} for menu in menus]
|
|
|
|
role = self.repository.get_role_by_code(session, role_code)
|
|
if role is not None:
|
|
relations = self.repository.list_role_menu_relations(session, role.id)
|
|
if relations:
|
|
allowed_menu_ids = {item.menu_id for item in relations}
|
|
filtered = [
|
|
{"menu_name": menu.menu_name, "menu_path": menu.menu_path}
|
|
for menu in menus
|
|
if menu.id in allowed_menu_ids and menu.menu_path
|
|
]
|
|
if filtered:
|
|
return filtered
|
|
|
|
allowed_paths = {item["menu_path"] for item in self.DEFAULT_ROLE_MENUS.get(role_code, [])}
|
|
filtered = [
|
|
{"menu_name": menu.menu_name, "menu_path": menu.menu_path}
|
|
for menu in menus
|
|
if menu.menu_path in allowed_paths
|
|
]
|
|
return filtered or self.DEFAULT_ROLE_MENUS.get(role_code, [])
|
|
|
|
def _load_role_permissions(self, session: Session, role_code: str) -> list[str]:
|
|
menus = self.repository.list_menus(session, {"status": 1})
|
|
if not menus:
|
|
return self.DEFAULT_ROLE_PERMISSIONS.get(role_code, [])
|
|
|
|
if role_code == "admin":
|
|
permissions = [menu.permission_code for menu in menus if menu.permission_code]
|
|
return permissions or self.DEFAULT_ROLE_PERMISSIONS.get(role_code, [])
|
|
|
|
role = self.repository.get_role_by_code(session, role_code)
|
|
if role is not None:
|
|
relations = self.repository.list_role_menu_relations(session, role.id)
|
|
if relations:
|
|
allowed_menu_ids = {item.menu_id for item in relations}
|
|
permissions = [
|
|
menu.permission_code
|
|
for menu in menus
|
|
if menu.id in allowed_menu_ids and menu.permission_code
|
|
]
|
|
if permissions:
|
|
return permissions
|
|
|
|
allowed = set(self.DEFAULT_ROLE_PERMISSIONS.get(role_code, []))
|
|
permissions = [menu.permission_code for menu in menus if menu.permission_code in allowed]
|
|
return permissions or self.DEFAULT_ROLE_PERMISSIONS.get(role_code, [])
|
|
|
|
def _load_revoked_tokens(self) -> dict[str, int]:
|
|
if not self.token_store_path.exists():
|
|
return {}
|
|
try:
|
|
payload = json.loads(self.token_store_path.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError):
|
|
return {}
|
|
now_ts = int(datetime.now(timezone.utc).timestamp())
|
|
tokens = {
|
|
token: int(exp)
|
|
for token, exp in payload.items()
|
|
if isinstance(token, str) and isinstance(exp, int) and exp > now_ts
|
|
}
|
|
if len(tokens) != len(payload):
|
|
self._save_revoked_tokens(tokens)
|
|
return tokens
|
|
|
|
def _save_revoked_tokens(self, tokens: dict[str, int]) -> None:
|
|
self.token_store_path.write_text(json.dumps(tokens, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
|
|
def _is_token_revoked(self, token: str) -> bool:
|
|
if not token:
|
|
return False
|
|
now_ts = int(datetime.now(timezone.utc).timestamp())
|
|
expired_tokens = [item for item, exp in self.revoked_tokens.items() if exp <= now_ts]
|
|
if expired_tokens:
|
|
for item in expired_tokens:
|
|
self.revoked_tokens.pop(item, None)
|
|
self._save_revoked_tokens(self.revoked_tokens)
|
|
return token in self.revoked_tokens
|
|
|
|
def _revoke_token(self, token: str) -> None:
|
|
payload = decode_access_token(token)
|
|
expire_at = int(payload.get("exp", 0)) if payload else int(datetime.now(timezone.utc).timestamp()) + 60
|
|
self.revoked_tokens[token] = expire_at
|
|
self._save_revoked_tokens(self.revoked_tokens)
|
|
|
|
|
|
auth_service = AuthService()
|