1216 lines
43 KiB
Python
1216 lines
43 KiB
Python
import hashlib
|
|
import hmac
|
|
import os
|
|
import secrets
|
|
import uuid
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
import bcrypt
|
|
import jwt
|
|
from sqlalchemy import delete
|
|
from sqlmodel import Session, select
|
|
|
|
from core.cache import cache
|
|
from core.settings import JWT_ALGORITHM, JWT_SECRET
|
|
from models.bot import BotInstance
|
|
from models.sys_auth import (
|
|
SysMenu,
|
|
SysPermission,
|
|
SysRole,
|
|
SysRoleMenu,
|
|
SysRolePermission,
|
|
SysUser,
|
|
SysUserBot,
|
|
)
|
|
|
|
DEFAULT_ADMIN_USERNAME = (str(os.getenv("SYS_ADMIN_DEFAULT_USERNAME") or "admin").strip().lower() or "admin")
|
|
DEFAULT_ADMIN_PASSWORD = str(os.getenv("SYS_ADMIN_DEFAULT_PASSWORD") or "admin123").strip() or "admin123"
|
|
PASSWORD_ITERATIONS = 120_000
|
|
AUTH_TOKEN_CACHE_PREFIX = "sys_auth:token:"
|
|
SUPER_ADMIN_ROLE_KEY = "super_admin"
|
|
NORMAL_USER_ROLE_KEY = "normal_user"
|
|
|
|
ROLE_SEEDS: List[Dict[str, Any]] = [
|
|
{
|
|
"role_key": SUPER_ADMIN_ROLE_KEY,
|
|
"name": "Super Admin",
|
|
"description": "平台超级管理员,拥有全部菜单与权限。",
|
|
"sort_order": 10,
|
|
},
|
|
{
|
|
"role_key": NORMAL_USER_ROLE_KEY,
|
|
"name": "Normal User",
|
|
"description": "普通用户,仅可访问已绑定的 Bot 和个人相关页面。",
|
|
"sort_order": 20,
|
|
},
|
|
]
|
|
|
|
MENU_SEEDS: List[Dict[str, Any]] = [
|
|
{
|
|
"menu_key": "general",
|
|
"parent_key": "",
|
|
"title": "General",
|
|
"title_en": "General",
|
|
"menu_type": "group",
|
|
"route_path": "",
|
|
"icon": "layout-grid",
|
|
"permission_key": "",
|
|
"sort_order": 10,
|
|
},
|
|
{
|
|
"menu_key": "general_dashboard",
|
|
"parent_key": "general",
|
|
"title": "Dashboard",
|
|
"title_en": "Dashboard",
|
|
"menu_type": "item",
|
|
"route_path": "/dashboard",
|
|
"icon": "layout-dashboard",
|
|
"permission_key": "general.dashboard.view",
|
|
"sort_order": 10,
|
|
},
|
|
{
|
|
"menu_key": "general_chat",
|
|
"parent_key": "general",
|
|
"title": "Chat",
|
|
"title_en": "Chat",
|
|
"menu_type": "item",
|
|
"route_path": "/chat",
|
|
"icon": "message-circle",
|
|
"permission_key": "general.chat.view",
|
|
"sort_order": 15,
|
|
},
|
|
{
|
|
"menu_key": "general_edge",
|
|
"parent_key": "general",
|
|
"title": "Edge 管理",
|
|
"title_en": "Edge Management",
|
|
"menu_type": "item",
|
|
"route_path": "/dashboard/edges",
|
|
"icon": "waypoints",
|
|
"permission_key": "general.edge.manage",
|
|
"sort_order": 20,
|
|
},
|
|
{
|
|
"menu_key": "admin",
|
|
"parent_key": "",
|
|
"title": "Admin",
|
|
"title_en": "Admin",
|
|
"menu_type": "group",
|
|
"route_path": "",
|
|
"icon": "shield",
|
|
"permission_key": "",
|
|
"sort_order": 20,
|
|
},
|
|
{
|
|
"menu_key": "admin_skills",
|
|
"parent_key": "admin",
|
|
"title": "技能管理",
|
|
"title_en": "Skill Management",
|
|
"menu_type": "item",
|
|
"route_path": "/admin/skills",
|
|
"icon": "wrench",
|
|
"permission_key": "admin.skills.manage",
|
|
"sort_order": 10,
|
|
},
|
|
{
|
|
"menu_key": "admin_settings",
|
|
"parent_key": "admin",
|
|
"title": "系统参数",
|
|
"title_en": "System Settings",
|
|
"menu_type": "item",
|
|
"route_path": "/admin/settings",
|
|
"icon": "sliders-horizontal",
|
|
"permission_key": "admin.settings.manage",
|
|
"sort_order": 20,
|
|
},
|
|
{
|
|
"menu_key": "admin_templates",
|
|
"parent_key": "admin",
|
|
"title": "模版管理",
|
|
"title_en": "Template Management",
|
|
"menu_type": "item",
|
|
"route_path": "/admin/templates",
|
|
"icon": "files",
|
|
"permission_key": "admin.templates.manage",
|
|
"sort_order": 30,
|
|
},
|
|
{
|
|
"menu_key": "admin_deploy",
|
|
"parent_key": "admin",
|
|
"title": "迁移/部署",
|
|
"title_en": "Migration / Deploy",
|
|
"menu_type": "item",
|
|
"route_path": "/admin/deploy",
|
|
"icon": "rocket",
|
|
"permission_key": "admin.deploy.manage",
|
|
"sort_order": 40,
|
|
},
|
|
{
|
|
"menu_key": "admin_users",
|
|
"parent_key": "admin",
|
|
"title": "用户管理",
|
|
"title_en": "User Management",
|
|
"menu_type": "item",
|
|
"route_path": "/admin/users",
|
|
"icon": "users",
|
|
"permission_key": "admin.users.manage",
|
|
"sort_order": 50,
|
|
},
|
|
{
|
|
"menu_key": "admin_roles",
|
|
"parent_key": "admin",
|
|
"title": "角色管理",
|
|
"title_en": "Role Management",
|
|
"menu_type": "item",
|
|
"route_path": "/admin/roles",
|
|
"icon": "badge-check",
|
|
"permission_key": "admin.roles.manage",
|
|
"sort_order": 60,
|
|
},
|
|
{
|
|
"menu_key": "profile",
|
|
"parent_key": "",
|
|
"title": "Profile",
|
|
"title_en": "Profile",
|
|
"menu_type": "group",
|
|
"route_path": "",
|
|
"icon": "user-round",
|
|
"permission_key": "",
|
|
"sort_order": 30,
|
|
},
|
|
{
|
|
"menu_key": "profile_usage_logs",
|
|
"parent_key": "profile",
|
|
"title": "使用日志",
|
|
"title_en": "Usage Logs",
|
|
"menu_type": "item",
|
|
"route_path": "/profile/usage-logs",
|
|
"icon": "history",
|
|
"permission_key": "profile.usage_logs.view",
|
|
"sort_order": 10,
|
|
},
|
|
{
|
|
"menu_key": "profile_api_tokens",
|
|
"parent_key": "profile",
|
|
"title": "API 令牌",
|
|
"title_en": "API Tokens",
|
|
"menu_type": "item",
|
|
"route_path": "/profile/api-tokens",
|
|
"icon": "key-round",
|
|
"permission_key": "profile.api_tokens.view",
|
|
"sort_order": 20,
|
|
},
|
|
]
|
|
|
|
PERMISSION_SEEDS: List[Dict[str, Any]] = [
|
|
{
|
|
"permission_key": "general.dashboard.view",
|
|
"name": "查看仪表板",
|
|
"menu_key": "general_dashboard",
|
|
"action": "view",
|
|
"description": "查看平台首页仪表板。",
|
|
"sort_order": 10,
|
|
},
|
|
{
|
|
"permission_key": "general.edge.manage",
|
|
"name": "管理 Edge 节点",
|
|
"menu_key": "general_edge",
|
|
"action": "manage",
|
|
"description": "查看与维护 Edge 节点。",
|
|
"sort_order": 20,
|
|
},
|
|
{
|
|
"permission_key": "general.chat.view",
|
|
"name": "访问 Bot Chat",
|
|
"menu_key": "general_chat",
|
|
"action": "view",
|
|
"description": "访问已绑定 Bot 的聊天与运行页。",
|
|
"sort_order": 25,
|
|
},
|
|
{
|
|
"permission_key": "admin.skills.manage",
|
|
"name": "管理技能市场",
|
|
"menu_key": "admin_skills",
|
|
"action": "manage",
|
|
"description": "管理技能包、上传归档与元数据。",
|
|
"sort_order": 30,
|
|
},
|
|
{
|
|
"permission_key": "admin.settings.manage",
|
|
"name": "管理系统参数",
|
|
"menu_key": "admin_settings",
|
|
"action": "manage",
|
|
"description": "维护系统参数与运行配置。",
|
|
"sort_order": 40,
|
|
},
|
|
{
|
|
"permission_key": "admin.templates.manage",
|
|
"name": "管理平台模板",
|
|
"menu_key": "admin_templates",
|
|
"action": "manage",
|
|
"description": "维护平台级模版内容。",
|
|
"sort_order": 50,
|
|
},
|
|
{
|
|
"permission_key": "admin.deploy.manage",
|
|
"name": "管理迁移部署",
|
|
"menu_key": "admin_deploy",
|
|
"action": "manage",
|
|
"description": "查看部署概览、迁移与部署入口。",
|
|
"sort_order": 60,
|
|
},
|
|
{
|
|
"permission_key": "admin.users.manage",
|
|
"name": "管理系统用户",
|
|
"menu_key": "admin_users",
|
|
"action": "manage",
|
|
"description": "管理后台用户、状态与默认资料。",
|
|
"sort_order": 70,
|
|
},
|
|
{
|
|
"permission_key": "admin.roles.manage",
|
|
"name": "管理系统角色",
|
|
"menu_key": "admin_roles",
|
|
"action": "manage",
|
|
"description": "管理角色与菜单、权限分配。",
|
|
"sort_order": 80,
|
|
},
|
|
{
|
|
"permission_key": "profile.usage_logs.view",
|
|
"name": "查看使用日志",
|
|
"menu_key": "profile_usage_logs",
|
|
"action": "view",
|
|
"description": "查看个人使用日志页面。",
|
|
"sort_order": 90,
|
|
},
|
|
{
|
|
"permission_key": "profile.api_tokens.view",
|
|
"name": "查看 API 令牌",
|
|
"menu_key": "profile_api_tokens",
|
|
"action": "view",
|
|
"description": "查看个人 API 令牌页面。",
|
|
"sort_order": 100,
|
|
},
|
|
]
|
|
|
|
LEGACY_MENU_KEYS = {"admin_permissions", "admin_menus"}
|
|
LEGACY_PERMISSION_KEYS = {"admin.permissions.manage", "admin.menus.manage"}
|
|
|
|
|
|
def _utcnow() -> datetime:
|
|
return datetime.utcnow()
|
|
|
|
|
|
def _dt_to_iso(value: Optional[datetime]) -> Optional[str]:
|
|
if value is None:
|
|
return None
|
|
return value.isoformat() + "Z"
|
|
|
|
|
|
def _new_salt() -> str:
|
|
return secrets.token_hex(16)
|
|
|
|
|
|
def hash_password(password: str, salt: str) -> str:
|
|
_ = salt
|
|
return bcrypt.hashpw(str(password or "").encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
|
|
|
|
|
def _verify_legacy_pbkdf2_password(password: str, salt: str, stored_hash: str) -> bool:
|
|
if not salt or not stored_hash:
|
|
return False
|
|
payload = hashlib.pbkdf2_hmac(
|
|
"sha256",
|
|
str(password or "").encode("utf-8"),
|
|
bytes.fromhex(str(salt or "")),
|
|
PASSWORD_ITERATIONS,
|
|
)
|
|
return hmac.compare_digest(payload.hex(), str(stored_hash or ""))
|
|
|
|
|
|
def verify_password(password: str, salt: str, stored_hash: str) -> bool:
|
|
if not stored_hash:
|
|
return False
|
|
normalized_hash = str(stored_hash or "")
|
|
if normalized_hash.startswith("$2a$") or normalized_hash.startswith("$2b$") or normalized_hash.startswith("$2y$"):
|
|
try:
|
|
return bool(bcrypt.checkpw(str(password or "").encode("utf-8"), normalized_hash.encode("utf-8")))
|
|
except Exception:
|
|
return False
|
|
return _verify_legacy_pbkdf2_password(password, salt, stored_hash)
|
|
|
|
|
|
def hash_token(token: str) -> str:
|
|
return hashlib.sha256(str(token or "").encode("utf-8")).hexdigest()
|
|
|
|
|
|
def _auth_token_cache_key(jti: str) -> str:
|
|
return f"{AUTH_TOKEN_CACHE_PREFIX}{str(jti or '').strip()}"
|
|
|
|
|
|
def _session_payload(user: SysUser, *, jti: str, expires_at: datetime) -> Dict[str, Any]:
|
|
return {
|
|
"sub": str(int(user.id or 0)),
|
|
"username": str(user.username or ""),
|
|
"jti": jti,
|
|
"iat": int(datetime.now(timezone.utc).timestamp()),
|
|
"exp": int(expires_at.replace(tzinfo=timezone.utc).timestamp()),
|
|
}
|
|
|
|
|
|
def _ensure_auth_cache_ready() -> None:
|
|
if not cache.enabled or not cache.ping():
|
|
raise RuntimeError("Redis is required for user session storage")
|
|
|
|
|
|
def _seed_roles(session: Session) -> Dict[str, SysRole]:
|
|
result: Dict[str, SysRole] = {}
|
|
for item in ROLE_SEEDS:
|
|
row = session.exec(select(SysRole).where(SysRole.role_key == item["role_key"])).first()
|
|
if row is None:
|
|
row = SysRole(role_key=item["role_key"])
|
|
row.name = item["name"]
|
|
row.description = item["description"]
|
|
row.is_active = True
|
|
row.sort_order = int(item["sort_order"])
|
|
row.updated_at = _utcnow()
|
|
session.add(row)
|
|
result[item["role_key"]] = row
|
|
session.commit()
|
|
for key in list(result.keys()):
|
|
result[key] = session.exec(select(SysRole).where(SysRole.role_key == key)).first()
|
|
return result
|
|
|
|
|
|
def _seed_menus(session: Session) -> Dict[str, SysMenu]:
|
|
result: Dict[str, SysMenu] = {}
|
|
for item in MENU_SEEDS:
|
|
row = session.exec(select(SysMenu).where(SysMenu.menu_key == item["menu_key"])).first()
|
|
if row is None:
|
|
row = SysMenu(menu_key=item["menu_key"])
|
|
row.parent_key = item["parent_key"]
|
|
row.title = item["title"]
|
|
row.title_en = item["title_en"]
|
|
row.menu_type = item["menu_type"]
|
|
row.route_path = item["route_path"]
|
|
row.icon = item["icon"]
|
|
row.permission_key = item["permission_key"]
|
|
row.visible = True
|
|
row.sort_order = int(item["sort_order"])
|
|
row.updated_at = _utcnow()
|
|
session.add(row)
|
|
result[item["menu_key"]] = row
|
|
session.commit()
|
|
for key in list(result.keys()):
|
|
result[key] = session.exec(select(SysMenu).where(SysMenu.menu_key == key)).first()
|
|
return result
|
|
|
|
|
|
def _cleanup_legacy_auth_entries(session: Session) -> None:
|
|
seeded_menu_keys = {str(item["menu_key"]) for item in MENU_SEEDS}
|
|
seeded_permission_keys = {str(item["permission_key"]) for item in PERMISSION_SEEDS}
|
|
|
|
obsolete_permissions = session.exec(
|
|
select(SysPermission).where(
|
|
(~SysPermission.permission_key.in_(seeded_permission_keys)) | SysPermission.permission_key.in_(LEGACY_PERMISSION_KEYS)
|
|
)
|
|
).all()
|
|
permission_ids = [int(item.id) for item in obsolete_permissions if item.id is not None]
|
|
if permission_ids:
|
|
session.exec(delete(SysRolePermission).where(SysRolePermission.permission_id.in_(permission_ids)))
|
|
session.exec(delete(SysPermission).where(SysPermission.id.in_(permission_ids)))
|
|
session.commit()
|
|
|
|
obsolete_menus = session.exec(
|
|
select(SysMenu).where((~SysMenu.menu_key.in_(seeded_menu_keys)) | SysMenu.menu_key.in_(LEGACY_MENU_KEYS))
|
|
).all()
|
|
menu_ids = [int(item.id) for item in obsolete_menus if item.id is not None]
|
|
if menu_ids:
|
|
session.exec(delete(SysRoleMenu).where(SysRoleMenu.menu_id.in_(menu_ids)))
|
|
session.exec(delete(SysMenu).where(SysMenu.id.in_(menu_ids)))
|
|
session.commit()
|
|
|
|
|
|
def _seed_permissions(session: Session) -> Dict[str, SysPermission]:
|
|
result: Dict[str, SysPermission] = {}
|
|
for item in PERMISSION_SEEDS:
|
|
row = session.exec(select(SysPermission).where(SysPermission.permission_key == item["permission_key"])).first()
|
|
if row is None:
|
|
row = SysPermission(permission_key=item["permission_key"])
|
|
row.name = item["name"]
|
|
row.menu_key = item["menu_key"]
|
|
row.action = item["action"]
|
|
row.description = item["description"]
|
|
row.sort_order = int(item["sort_order"])
|
|
row.updated_at = _utcnow()
|
|
session.add(row)
|
|
result[item["permission_key"]] = row
|
|
session.commit()
|
|
for key in list(result.keys()):
|
|
result[key] = session.exec(select(SysPermission).where(SysPermission.permission_key == key)).first()
|
|
return result
|
|
|
|
|
|
def _ensure_role_menu(session: Session, role_id: int, menu_id: int) -> None:
|
|
exists = session.exec(
|
|
select(SysRoleMenu).where(SysRoleMenu.role_id == role_id, SysRoleMenu.menu_id == menu_id)
|
|
).first()
|
|
if exists is None:
|
|
session.add(SysRoleMenu(role_id=role_id, menu_id=menu_id))
|
|
|
|
|
|
def _ensure_role_permission(session: Session, role_id: int, permission_id: int) -> None:
|
|
exists = session.exec(
|
|
select(SysRolePermission).where(
|
|
SysRolePermission.role_id == role_id,
|
|
SysRolePermission.permission_id == permission_id,
|
|
)
|
|
).first()
|
|
if exists is None:
|
|
session.add(SysRolePermission(role_id=role_id, permission_id=permission_id))
|
|
|
|
|
|
def seed_sys_auth(session: Session) -> None:
|
|
_cleanup_legacy_auth_entries(session)
|
|
roles = _seed_roles(session)
|
|
menus = _seed_menus(session)
|
|
permissions = _seed_permissions(session)
|
|
|
|
admin_role = roles[SUPER_ADMIN_ROLE_KEY]
|
|
admin_has_menu_bindings = False
|
|
admin_has_permission_bindings = False
|
|
if admin_role.id is not None:
|
|
admin_has_menu_bindings = (
|
|
session.exec(select(SysRoleMenu).where(SysRoleMenu.role_id == admin_role.id)).first() is not None
|
|
)
|
|
admin_has_permission_bindings = (
|
|
session.exec(select(SysRolePermission).where(SysRolePermission.role_id == admin_role.id)).first() is not None
|
|
)
|
|
|
|
if admin_role.id is not None and not admin_has_menu_bindings:
|
|
for menu in menus.values():
|
|
if menu.id is not None:
|
|
_ensure_role_menu(session, admin_role.id, menu.id)
|
|
|
|
if admin_role.id is not None and not admin_has_permission_bindings:
|
|
for permission in permissions.values():
|
|
if permission.id is not None:
|
|
_ensure_role_permission(session, admin_role.id, permission.id)
|
|
|
|
normal_user_role = roles.get(NORMAL_USER_ROLE_KEY)
|
|
if normal_user_role is not None and normal_user_role.id is not None:
|
|
normal_user_menu_keys = {"general_chat", "profile_usage_logs", "profile_api_tokens"}
|
|
normal_user_permission_keys = {
|
|
"general.chat.view",
|
|
"profile.usage_logs.view",
|
|
"profile.api_tokens.view",
|
|
}
|
|
for menu_key in normal_user_menu_keys:
|
|
menu = menus.get(menu_key)
|
|
if menu is not None and menu.id is not None:
|
|
_ensure_role_menu(session, int(normal_user_role.id), int(menu.id))
|
|
for permission_key in normal_user_permission_keys:
|
|
permission = permissions.get(permission_key)
|
|
if permission is not None and permission.id is not None:
|
|
_ensure_role_permission(session, int(normal_user_role.id), int(permission.id))
|
|
session.commit()
|
|
|
|
user_count = session.exec(select(SysUser)).all()
|
|
if len(user_count) == 0:
|
|
salt = _new_salt()
|
|
session.add(
|
|
SysUser(
|
|
username=DEFAULT_ADMIN_USERNAME,
|
|
display_name="Administrator",
|
|
password_salt=salt,
|
|
password_hash=hash_password(DEFAULT_ADMIN_PASSWORD, salt),
|
|
role_id=admin_role.id,
|
|
is_active=True,
|
|
created_at=_utcnow(),
|
|
updated_at=_utcnow(),
|
|
)
|
|
)
|
|
session.commit()
|
|
|
|
|
|
def _normalize_bot_ids(bot_ids: List[str] | None) -> List[str]:
|
|
normalized: List[str] = []
|
|
for item in list(bot_ids or []):
|
|
bot_id = str(item or "").strip()
|
|
if bot_id and bot_id not in normalized:
|
|
normalized.append(bot_id)
|
|
return normalized
|
|
|
|
|
|
def _invalidate_user_bot_access_cache(user_id: int) -> None:
|
|
normalized_user_id = int(user_id or 0)
|
|
if normalized_user_id <= 0:
|
|
return
|
|
cache.delete(f"bots:list:user:{normalized_user_id}")
|
|
|
|
|
|
def _sync_user_bot_bindings(session: Session, *, user_id: int, bot_ids: List[str]) -> None:
|
|
session.exec(delete(SysUserBot).where(SysUserBot.user_id == user_id))
|
|
normalized_bot_ids = _normalize_bot_ids(bot_ids)
|
|
if normalized_bot_ids:
|
|
rows = session.exec(select(BotInstance.id).where(BotInstance.id.in_(normalized_bot_ids))).all()
|
|
existing_bot_ids = {str(item or "").strip() for item in rows}
|
|
missing = [bot_id for bot_id in normalized_bot_ids if bot_id not in existing_bot_ids]
|
|
if missing:
|
|
raise ValueError(f"Bots not found: {', '.join(missing[:5])}")
|
|
for bot_id in normalized_bot_ids:
|
|
session.add(SysUserBot(user_id=user_id, bot_id=bot_id))
|
|
session.commit()
|
|
_invalidate_user_bot_access_cache(user_id)
|
|
|
|
|
|
def _list_user_bot_ids(session: Session, user_id: int) -> List[str]:
|
|
rows = session.exec(
|
|
select(SysUserBot.bot_id).where(SysUserBot.user_id == user_id).order_by(SysUserBot.bot_id.asc())
|
|
).all()
|
|
return [str(item or "").strip() for item in rows if str(item or "").strip()]
|
|
|
|
|
|
def _list_user_assigned_bots(session: Session, user_id: int) -> List[Dict[str, Any]]:
|
|
bound_bot_ids = _list_user_bot_ids(session, user_id)
|
|
if not bound_bot_ids:
|
|
return []
|
|
bots = session.exec(select(BotInstance).where(BotInstance.id.in_(bound_bot_ids))).all()
|
|
bot_map = {str(bot.id or "").strip(): bot for bot in bots if str(bot.id or "").strip()}
|
|
items: List[Dict[str, Any]] = []
|
|
for bot_id in bound_bot_ids:
|
|
bot = bot_map.get(bot_id)
|
|
if bot is None:
|
|
continue
|
|
items.append(
|
|
{
|
|
"id": bot_id,
|
|
"name": str(bot.name or bot_id),
|
|
"enabled": bool(getattr(bot, "enabled", True)),
|
|
"node_id": str(getattr(bot, "node_id", "") or ""),
|
|
"node_display_name": str(getattr(bot, "node_id", "") or ""),
|
|
"docker_status": str(getattr(bot, "docker_status", "STOPPED") or "STOPPED"),
|
|
"image_tag": str(getattr(bot, "image_tag", "") or ""),
|
|
}
|
|
)
|
|
return items
|
|
|
|
|
|
def _is_super_admin(role: Optional[SysRole]) -> bool:
|
|
return str(getattr(role, "role_key", "") or "") == SUPER_ADMIN_ROLE_KEY
|
|
|
|
|
|
def find_user_by_username(session: Session, username: str) -> Optional[SysUser]:
|
|
normalized = str(username or "").strip().lower()
|
|
if not normalized:
|
|
return None
|
|
return session.exec(select(SysUser).where(SysUser.username == normalized)).first()
|
|
|
|
|
|
def authenticate_user(session: Session, username: str, password: str) -> Optional[SysUser]:
|
|
user = find_user_by_username(session, username)
|
|
if user is None or not bool(user.is_active):
|
|
return None
|
|
stored_hash = str(user.password_hash or "")
|
|
stored_salt = str(user.password_salt or "")
|
|
if not verify_password(password, stored_salt, stored_hash):
|
|
return None
|
|
if stored_hash and not stored_hash.startswith("$2"):
|
|
user.password_hash = hash_password(password, "")
|
|
user.password_salt = ""
|
|
user.updated_at = _utcnow()
|
|
session.add(user)
|
|
session.commit()
|
|
session.refresh(user)
|
|
return user
|
|
|
|
|
|
def issue_user_token(session: Session, user: SysUser) -> tuple[str, datetime]:
|
|
_ensure_auth_cache_ready()
|
|
from services.platform_settings_service import get_sys_auth_token_ttl_days
|
|
|
|
ttl_days = get_sys_auth_token_ttl_days(session)
|
|
expires_at = _utcnow() + timedelta(days=ttl_days)
|
|
jti = uuid.uuid4().hex
|
|
token = jwt.encode(_session_payload(user, jti=jti, expires_at=expires_at), JWT_SECRET, algorithm=JWT_ALGORITHM)
|
|
cache.set_json(
|
|
_auth_token_cache_key(jti),
|
|
{
|
|
"user_id": int(user.id or 0),
|
|
"username": str(user.username or ""),
|
|
"token_hash": hash_token(token),
|
|
},
|
|
ttl=max(1, ttl_days * 24 * 60 * 60),
|
|
)
|
|
user.current_token_hash = None
|
|
user.current_token_expires_at = None
|
|
user.last_login_at = _utcnow()
|
|
user.updated_at = _utcnow()
|
|
session.add(user)
|
|
session.commit()
|
|
session.refresh(user)
|
|
return token, expires_at
|
|
|
|
|
|
def revoke_user_token(token: str) -> None:
|
|
try:
|
|
payload = jwt.decode(str(token or "").strip(), JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
|
except Exception:
|
|
return
|
|
jti = str(payload.get("jti") or "").strip()
|
|
if not jti:
|
|
return
|
|
cache.delete(_auth_token_cache_key(jti))
|
|
|
|
|
|
def resolve_user_by_token(session: Session, token: str) -> Optional[SysUser]:
|
|
candidate = str(token or "").strip()
|
|
if not candidate:
|
|
return None
|
|
try:
|
|
payload = jwt.decode(candidate, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
|
except Exception:
|
|
return None
|
|
user_id = int(payload.get("sub") or 0)
|
|
jti = str(payload.get("jti") or "").strip()
|
|
if user_id <= 0 or not jti:
|
|
return None
|
|
cached = cache.get_json(_auth_token_cache_key(jti))
|
|
if not isinstance(cached, dict):
|
|
return None
|
|
if int(cached.get("user_id") or 0) != user_id:
|
|
return None
|
|
if str(cached.get("token_hash") or "").strip() != hash_token(candidate):
|
|
return None
|
|
user = session.get(SysUser, user_id)
|
|
if user is None or not bool(user.is_active):
|
|
return None
|
|
return user
|
|
|
|
|
|
def _role_payload(role: Optional[SysRole]) -> Optional[Dict[str, Any]]:
|
|
if role is None:
|
|
return None
|
|
return {
|
|
"id": int(role.id or 0),
|
|
"role_key": role.role_key,
|
|
"name": role.name,
|
|
}
|
|
|
|
|
|
def _list_role_permissions(session: Session, role_id: Optional[int]) -> List[str]:
|
|
if not role_id:
|
|
return []
|
|
rows = session.exec(
|
|
select(SysPermission)
|
|
.join(SysRolePermission, SysRolePermission.permission_id == SysPermission.id)
|
|
.where(SysRolePermission.role_id == role_id)
|
|
.order_by(SysPermission.sort_order.asc(), SysPermission.permission_key.asc())
|
|
).all()
|
|
return [str(row.permission_key or "").strip() for row in rows if str(row.permission_key or "").strip()]
|
|
|
|
|
|
def _list_role_menus(session: Session, role_id: Optional[int]) -> List[SysMenu]:
|
|
if not role_id:
|
|
return []
|
|
explicit_menus = session.exec(
|
|
select(SysMenu)
|
|
.join(SysRoleMenu, SysRoleMenu.menu_id == SysMenu.id)
|
|
.where(SysRoleMenu.role_id == role_id, SysMenu.visible == True)
|
|
.order_by(SysMenu.sort_order.asc(), SysMenu.id.asc())
|
|
).all()
|
|
permission_bound_menus = session.exec(
|
|
select(SysMenu)
|
|
.join(SysPermission, SysPermission.menu_key == SysMenu.menu_key)
|
|
.join(SysRolePermission, SysRolePermission.permission_id == SysPermission.id)
|
|
.where(SysRolePermission.role_id == role_id, SysMenu.visible == True)
|
|
.order_by(SysMenu.sort_order.asc(), SysMenu.id.asc())
|
|
).all()
|
|
|
|
menu_map: Dict[str, SysMenu] = {}
|
|
for row in [*explicit_menus, *permission_bound_menus]:
|
|
key = str(row.menu_key or "").strip()
|
|
if key:
|
|
menu_map[key] = row
|
|
|
|
if not menu_map:
|
|
return []
|
|
|
|
all_visible_menus = session.exec(select(SysMenu).where(SysMenu.visible == True)).all()
|
|
all_menu_map = {str(row.menu_key or "").strip(): row for row in all_visible_menus if str(row.menu_key or "").strip()}
|
|
pending_parent_keys = [str(row.parent_key or "").strip() for row in menu_map.values() if str(row.parent_key or "").strip()]
|
|
while pending_parent_keys:
|
|
parent_key = pending_parent_keys.pop()
|
|
if not parent_key or parent_key in menu_map:
|
|
continue
|
|
parent = all_menu_map.get(parent_key)
|
|
if parent is None:
|
|
continue
|
|
menu_map[parent_key] = parent
|
|
next_parent_key = str(parent.parent_key or "").strip()
|
|
if next_parent_key and next_parent_key not in menu_map:
|
|
pending_parent_keys.append(next_parent_key)
|
|
|
|
return sorted(menu_map.values(), key=lambda row: (int(row.sort_order or 100), int(row.id or 0)))
|
|
|
|
|
|
def _build_menu_tree(rows: List[SysMenu]) -> List[Dict[str, Any]]:
|
|
menu_map: Dict[str, Dict[str, Any]] = {}
|
|
roots: List[Dict[str, Any]] = []
|
|
for row in rows:
|
|
menu_key = str(row.menu_key or "").strip()
|
|
if not menu_key:
|
|
continue
|
|
menu_map[menu_key] = {
|
|
"menu_key": menu_key,
|
|
"parent_key": str(row.parent_key or "").strip(),
|
|
"title": row.title,
|
|
"title_en": row.title_en,
|
|
"menu_type": row.menu_type,
|
|
"route_path": row.route_path,
|
|
"icon": row.icon,
|
|
"permission_key": row.permission_key,
|
|
"sort_order": int(row.sort_order or 100),
|
|
"children": [],
|
|
}
|
|
for item in sorted(menu_map.values(), key=lambda value: (value["sort_order"], value["menu_key"])):
|
|
parent_key = str(item["parent_key"] or "").strip()
|
|
if parent_key and parent_key in menu_map:
|
|
menu_map[parent_key]["children"].append(item)
|
|
else:
|
|
roots.append(item)
|
|
return roots
|
|
|
|
|
|
def _normalize_role_key(value: str) -> str:
|
|
return str(value or "").strip().lower().replace(" ", "_")
|
|
|
|
|
|
def _normalize_username(value: str) -> str:
|
|
return str(value or "").strip().lower()
|
|
|
|
|
|
def _sync_role_bindings(
|
|
session: Session,
|
|
role: SysRole,
|
|
*,
|
|
menu_keys: List[str],
|
|
permission_keys: List[str],
|
|
) -> None:
|
|
role_id = int(role.id or 0)
|
|
if role_id <= 0:
|
|
raise ValueError("Role id is required")
|
|
|
|
normalized_menu_keys = sorted({str(key or "").strip() for key in menu_keys if str(key or "").strip()})
|
|
normalized_permission_keys = sorted({str(key or "").strip() for key in permission_keys if str(key or "").strip()})
|
|
|
|
session.exec(delete(SysRoleMenu).where(SysRoleMenu.role_id == role_id))
|
|
session.exec(delete(SysRolePermission).where(SysRolePermission.role_id == role_id))
|
|
session.commit()
|
|
|
|
if normalized_menu_keys:
|
|
menus = session.exec(select(SysMenu).where(SysMenu.menu_key.in_(normalized_menu_keys))).all()
|
|
menu_map = {str(row.menu_key or "").strip(): row for row in menus}
|
|
for menu_key in normalized_menu_keys:
|
|
row = menu_map.get(menu_key)
|
|
if row is not None and row.id is not None:
|
|
session.add(SysRoleMenu(role_id=role_id, menu_id=int(row.id)))
|
|
|
|
if normalized_permission_keys:
|
|
permissions = session.exec(
|
|
select(SysPermission).where(SysPermission.permission_key.in_(normalized_permission_keys))
|
|
).all()
|
|
permission_map = {str(row.permission_key or "").strip(): row for row in permissions}
|
|
for permission_key in normalized_permission_keys:
|
|
row = permission_map.get(permission_key)
|
|
if row is not None and row.id is not None:
|
|
session.add(SysRolePermission(role_id=role_id, permission_id=int(row.id)))
|
|
session.commit()
|
|
|
|
|
|
def list_sys_users(session: Session) -> List[Dict[str, Any]]:
|
|
roles = session.exec(select(SysRole)).all()
|
|
role_map = {int(role.id or 0): role for role in roles if role.id is not None}
|
|
users = session.exec(select(SysUser).order_by(SysUser.updated_at.desc(), SysUser.id.desc())).all()
|
|
binding_rows = session.exec(select(SysUserBot)).all()
|
|
bot_ids_map: Dict[int, List[str]] = {}
|
|
for row in binding_rows:
|
|
user_id = int(row.user_id or 0)
|
|
bot_id = str(row.bot_id or "").strip()
|
|
if user_id <= 0 or not bot_id:
|
|
continue
|
|
bot_ids_map.setdefault(user_id, []).append(bot_id)
|
|
result: List[Dict[str, Any]] = []
|
|
for user in users:
|
|
role = role_map.get(int(user.role_id or 0))
|
|
result.append(
|
|
{
|
|
"id": int(user.id or 0),
|
|
"username": str(user.username or ""),
|
|
"display_name": str(user.display_name or user.username or ""),
|
|
"is_active": bool(user.is_active),
|
|
"last_login_at": _dt_to_iso(user.last_login_at),
|
|
"role": _role_payload(role),
|
|
"bot_ids": sorted(bot_ids_map.get(int(user.id or 0), [])),
|
|
}
|
|
)
|
|
return result
|
|
|
|
|
|
def create_sys_user(
|
|
session: Session,
|
|
*,
|
|
username: str,
|
|
display_name: str,
|
|
password: str,
|
|
role_id: int,
|
|
is_active: bool,
|
|
bot_ids: Optional[List[str]] = None,
|
|
) -> Dict[str, Any]:
|
|
normalized_username = _normalize_username(username)
|
|
if not normalized_username:
|
|
raise ValueError("Username is required")
|
|
if find_user_by_username(session, normalized_username) is not None:
|
|
raise ValueError("Username already exists")
|
|
if len(str(password or "")) < 6:
|
|
raise ValueError("Password must be at least 6 characters")
|
|
role = session.get(SysRole, role_id)
|
|
if role is None:
|
|
raise ValueError("Role not found")
|
|
salt = _new_salt()
|
|
row = SysUser(
|
|
username=normalized_username,
|
|
display_name=str(display_name or "").strip() or normalized_username,
|
|
password_salt=salt,
|
|
password_hash=hash_password(password, salt),
|
|
role_id=int(role.id or 0),
|
|
is_active=bool(is_active),
|
|
created_at=_utcnow(),
|
|
updated_at=_utcnow(),
|
|
)
|
|
session.add(row)
|
|
session.commit()
|
|
session.refresh(row)
|
|
_sync_user_bot_bindings(session, user_id=int(row.id or 0), bot_ids=list(bot_ids or []))
|
|
return {
|
|
"id": int(row.id or 0),
|
|
"username": str(row.username or ""),
|
|
"display_name": str(row.display_name or row.username or ""),
|
|
"is_active": bool(row.is_active),
|
|
"last_login_at": _dt_to_iso(row.last_login_at),
|
|
"role": _role_payload(role),
|
|
"bot_ids": _list_user_bot_ids(session, int(row.id or 0)),
|
|
}
|
|
|
|
|
|
def update_sys_user(
|
|
session: Session,
|
|
*,
|
|
user_id: int,
|
|
display_name: str,
|
|
password: str,
|
|
role_id: int,
|
|
is_active: bool,
|
|
bot_ids: Optional[List[str]] = None,
|
|
acting_user_id: int = 0,
|
|
) -> Dict[str, Any]:
|
|
row = session.get(SysUser, user_id)
|
|
if row is None:
|
|
raise ValueError("User not found")
|
|
if acting_user_id > 0 and int(row.id or 0) == acting_user_id and not is_active:
|
|
raise ValueError("Current user cannot be disabled")
|
|
role = session.get(SysRole, role_id)
|
|
if role is None:
|
|
raise ValueError("Role not found")
|
|
row.display_name = str(display_name or "").strip() or str(row.username or "")
|
|
row.role_id = int(role.id or 0)
|
|
row.is_active = bool(is_active)
|
|
if str(password or "").strip():
|
|
if len(str(password or "")) < 6:
|
|
raise ValueError("Password must be at least 6 characters")
|
|
row.password_hash = hash_password(password, "")
|
|
row.password_salt = ""
|
|
row.updated_at = _utcnow()
|
|
session.add(row)
|
|
session.commit()
|
|
_invalidate_user_bot_access_cache(int(row.id or 0))
|
|
_sync_user_bot_bindings(session, user_id=int(row.id or 0), bot_ids=list(bot_ids or []))
|
|
session.refresh(row)
|
|
return {
|
|
"id": int(row.id or 0),
|
|
"username": str(row.username or ""),
|
|
"display_name": str(row.display_name or row.username or ""),
|
|
"is_active": bool(row.is_active),
|
|
"last_login_at": _dt_to_iso(row.last_login_at),
|
|
"role": _role_payload(role),
|
|
"bot_ids": _list_user_bot_ids(session, int(row.id or 0)),
|
|
}
|
|
|
|
|
|
def update_current_sys_user_profile(
|
|
session: Session,
|
|
*,
|
|
user_id: int,
|
|
display_name: str,
|
|
password: str,
|
|
) -> SysUser:
|
|
row = session.get(SysUser, user_id)
|
|
if row is None:
|
|
raise ValueError("User not found")
|
|
row.display_name = str(display_name or "").strip() or str(row.username or "")
|
|
if str(password or "").strip():
|
|
if len(str(password or "")) < 6:
|
|
raise ValueError("Password must be at least 6 characters")
|
|
row.password_hash = hash_password(password, "")
|
|
row.password_salt = ""
|
|
row.updated_at = _utcnow()
|
|
session.add(row)
|
|
session.commit()
|
|
session.refresh(row)
|
|
return row
|
|
|
|
|
|
def delete_sys_user(session: Session, *, user_id: int, acting_user_id: int = 0) -> None:
|
|
row = session.get(SysUser, user_id)
|
|
if row is None:
|
|
return
|
|
if acting_user_id > 0 and int(row.id or 0) == acting_user_id:
|
|
raise ValueError("Current user cannot be deleted")
|
|
session.exec(delete(SysUserBot).where(SysUserBot.user_id == user_id))
|
|
session.commit()
|
|
_invalidate_user_bot_access_cache(user_id)
|
|
session.delete(row)
|
|
session.commit()
|
|
|
|
|
|
def list_sys_roles(session: Session) -> List[Dict[str, Any]]:
|
|
roles = session.exec(select(SysRole).order_by(SysRole.sort_order.asc(), SysRole.id.asc())).all()
|
|
users = session.exec(select(SysUser)).all()
|
|
user_count_map: Dict[int, int] = {}
|
|
for user in users:
|
|
role_id = int(user.role_id or 0)
|
|
if role_id > 0:
|
|
user_count_map[role_id] = user_count_map.get(role_id, 0) + 1
|
|
result: List[Dict[str, Any]] = []
|
|
for role in roles:
|
|
role_id = int(role.id or 0)
|
|
result.append(
|
|
{
|
|
"id": role_id,
|
|
"role_key": str(role.role_key or ""),
|
|
"name": str(role.name or ""),
|
|
"description": str(role.description or ""),
|
|
"is_active": bool(role.is_active),
|
|
"sort_order": int(role.sort_order or 100),
|
|
"user_count": user_count_map.get(role_id, 0),
|
|
"menu_keys": [str(key) for key in _list_role_menu_keys(session, role_id)],
|
|
"permission_keys": [str(key) for key in _list_role_permissions(session, role_id)],
|
|
}
|
|
)
|
|
return result
|
|
|
|
|
|
def _list_role_menu_keys(session: Session, role_id: Optional[int]) -> List[str]:
|
|
rows = _list_role_menus(session, role_id)
|
|
return [str(row.menu_key or "").strip() for row in rows if str(row.menu_key or "").strip()]
|
|
|
|
|
|
def list_role_grant_bootstrap(session: Session) -> Dict[str, Any]:
|
|
menus = session.exec(
|
|
select(SysMenu)
|
|
.where(SysMenu.visible == True)
|
|
.order_by(SysMenu.sort_order.asc(), SysMenu.id.asc())
|
|
).all()
|
|
permissions = session.exec(
|
|
select(SysPermission).order_by(SysPermission.sort_order.asc(), SysPermission.permission_key.asc())
|
|
).all()
|
|
menu_rows = [
|
|
{
|
|
"menu_key": str(row.menu_key or ""),
|
|
"parent_key": str(row.parent_key or ""),
|
|
"title": str(row.title or ""),
|
|
"title_en": str(row.title_en or ""),
|
|
"menu_type": str(row.menu_type or "item"),
|
|
"route_path": str(row.route_path or ""),
|
|
"icon": str(row.icon or ""),
|
|
"sort_order": int(row.sort_order or 100),
|
|
"children": [],
|
|
}
|
|
for row in menus
|
|
]
|
|
return {
|
|
"menus": _build_menu_tree_from_dict(menu_rows),
|
|
"permissions": [
|
|
{
|
|
"id": int(row.id or 0),
|
|
"permission_key": str(row.permission_key or ""),
|
|
"name": str(row.name or ""),
|
|
"menu_key": str(row.menu_key or ""),
|
|
"action": str(row.action or "view"),
|
|
"description": str(row.description or ""),
|
|
"sort_order": int(row.sort_order or 100),
|
|
}
|
|
for row in permissions
|
|
],
|
|
}
|
|
|
|
|
|
def _build_menu_tree_from_dict(rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
menu_map = {str(item["menu_key"]): {**item, "children": []} for item in rows if str(item.get("menu_key") or "").strip()}
|
|
roots: List[Dict[str, Any]] = []
|
|
for item in sorted(menu_map.values(), key=lambda value: (int(value.get("sort_order") or 100), str(value.get("menu_key") or ""))):
|
|
parent_key = str(item.get("parent_key") or "").strip()
|
|
if parent_key and parent_key in menu_map:
|
|
menu_map[parent_key]["children"].append(item)
|
|
else:
|
|
roots.append(item)
|
|
return roots
|
|
|
|
|
|
def create_sys_role(
|
|
session: Session,
|
|
*,
|
|
role_key: str,
|
|
name: str,
|
|
description: str,
|
|
is_active: bool,
|
|
sort_order: int,
|
|
menu_keys: List[str],
|
|
permission_keys: List[str],
|
|
) -> Dict[str, Any]:
|
|
normalized_role_key = _normalize_role_key(role_key)
|
|
if not normalized_role_key:
|
|
raise ValueError("Role key is required")
|
|
exists = session.exec(select(SysRole).where(SysRole.role_key == normalized_role_key)).first()
|
|
if exists is not None:
|
|
raise ValueError("Role key already exists")
|
|
row = SysRole(
|
|
role_key=normalized_role_key,
|
|
name=str(name or "").strip() or normalized_role_key,
|
|
description=str(description or "").strip(),
|
|
is_active=bool(is_active),
|
|
sort_order=int(sort_order or 100),
|
|
created_at=_utcnow(),
|
|
updated_at=_utcnow(),
|
|
)
|
|
session.add(row)
|
|
session.commit()
|
|
session.refresh(row)
|
|
_sync_role_bindings(session, row, menu_keys=menu_keys, permission_keys=permission_keys)
|
|
return get_sys_role_detail(session, int(row.id or 0))
|
|
|
|
|
|
def update_sys_role(
|
|
session: Session,
|
|
*,
|
|
role_id: int,
|
|
name: str,
|
|
description: str,
|
|
is_active: bool,
|
|
sort_order: int,
|
|
menu_keys: List[str],
|
|
permission_keys: List[str],
|
|
) -> Dict[str, Any]:
|
|
row = session.get(SysRole, role_id)
|
|
if row is None:
|
|
raise ValueError("Role not found")
|
|
if str(row.role_key or "") == "super_admin" and not bool(is_active):
|
|
raise ValueError("Super Admin cannot be disabled")
|
|
row.name = str(name or "").strip() or str(row.role_key or "")
|
|
row.description = str(description or "").strip()
|
|
row.is_active = bool(is_active)
|
|
row.sort_order = int(sort_order or 100)
|
|
row.updated_at = _utcnow()
|
|
session.add(row)
|
|
session.commit()
|
|
session.refresh(row)
|
|
_sync_role_bindings(session, row, menu_keys=menu_keys, permission_keys=permission_keys)
|
|
return get_sys_role_detail(session, role_id)
|
|
|
|
|
|
def get_sys_role_detail(session: Session, role_id: int) -> Dict[str, Any]:
|
|
role = session.get(SysRole, role_id)
|
|
if role is None:
|
|
raise ValueError("Role not found")
|
|
users = session.exec(select(SysUser).where(SysUser.role_id == role_id)).all()
|
|
return {
|
|
"id": int(role.id or 0),
|
|
"role_key": str(role.role_key or ""),
|
|
"name": str(role.name or ""),
|
|
"description": str(role.description or ""),
|
|
"is_active": bool(role.is_active),
|
|
"sort_order": int(role.sort_order or 100),
|
|
"user_count": len(users),
|
|
"menu_keys": _list_role_menu_keys(session, role_id),
|
|
"permission_keys": _list_role_permissions(session, role_id),
|
|
}
|
|
|
|
|
|
def delete_sys_role(session: Session, *, role_id: int) -> None:
|
|
row = session.get(SysRole, role_id)
|
|
if row is None:
|
|
return
|
|
if str(row.role_key or "") == SUPER_ADMIN_ROLE_KEY:
|
|
raise ValueError("Super Admin cannot be deleted")
|
|
bound_users = session.exec(select(SysUser).where(SysUser.role_id == role_id)).all()
|
|
if bound_users:
|
|
raise ValueError("Role is still assigned to users")
|
|
session.exec(delete(SysRoleMenu).where(SysRoleMenu.role_id == role_id))
|
|
session.exec(delete(SysRolePermission).where(SysRolePermission.role_id == role_id))
|
|
session.commit()
|
|
session.delete(row)
|
|
session.commit()
|
|
|
|
|
|
def build_user_bootstrap(session: Session, user: SysUser, *, token: str = "", expires_at: Optional[datetime] = None) -> Dict[str, Any]:
|
|
role = session.get(SysRole, user.role_id) if user.role_id else None
|
|
permissions = _list_role_permissions(session, user.role_id)
|
|
menus = _build_menu_tree(_list_role_menus(session, user.role_id))
|
|
assigned_bots = [] if _is_super_admin(role) else _list_user_assigned_bots(session, int(user.id or 0))
|
|
home_path = "/dashboard"
|
|
for group in menus:
|
|
children = list(group.get("children") or [])
|
|
if children:
|
|
home_path = str(children[0].get("route_path") or "/dashboard")
|
|
break
|
|
return {
|
|
"token": token,
|
|
"expires_at": _dt_to_iso(expires_at),
|
|
"user": {
|
|
"id": int(user.id or 0),
|
|
"username": str(user.username or ""),
|
|
"display_name": str(user.display_name or user.username or ""),
|
|
"role": _role_payload(role),
|
|
},
|
|
"menus": menus,
|
|
"permissions": permissions,
|
|
"home_path": home_path,
|
|
"assigned_bots": assigned_bots,
|
|
}
|
|
|
|
|
|
def list_accessible_bots_for_user(session: Session, user: SysUser) -> List[BotInstance]:
|
|
role = session.get(SysRole, user.role_id) if user.role_id else None
|
|
if _is_super_admin(role):
|
|
return session.exec(select(BotInstance).order_by(BotInstance.updated_at.desc(), BotInstance.id.asc())).all()
|
|
bot_ids = _list_user_bot_ids(session, int(user.id or 0))
|
|
if not bot_ids:
|
|
return []
|
|
bots = session.exec(select(BotInstance).where(BotInstance.id.in_(bot_ids))).all()
|
|
bot_map = {str(bot.id or "").strip(): bot for bot in bots if str(bot.id or "").strip()}
|
|
return [bot_map[bot_id] for bot_id in bot_ids if bot_id in bot_map]
|
|
|
|
|
|
def user_can_access_bot(session: Session, user: SysUser, bot_id: str) -> bool:
|
|
normalized_bot_id = str(bot_id or "").strip()
|
|
if not normalized_bot_id:
|
|
return False
|
|
role = session.get(SysRole, user.role_id) if user.role_id else None
|
|
if _is_super_admin(role):
|
|
return True
|
|
row = session.exec(
|
|
select(SysUserBot).where(SysUserBot.user_id == int(user.id or 0), SysUserBot.bot_id == normalized_bot_id)
|
|
).first()
|
|
return row is not None
|