feat: add user and role management pages with API integration

- Implemented user management functionality in UsersPage including user creation, editing, deletion, and role assignment.
- Added role management functionality in RolesPage with role creation, editing, deletion, and path rule management.
- Created users API for handling user-related operations.
- Created roles API for handling role-related operations.
- Integrated permissions handling in both user and role management.
- Enhanced UI with Ant Design components for better user experience.
This commit is contained in:
shiyu
2026-01-30 15:59:22 +08:00
parent 4a2e01196d
commit e6ab01ef9d
33 changed files with 3462 additions and 10 deletions

View File

@@ -140,6 +140,7 @@ class AuthService:
email=user.email,
full_name=user.full_name,
disabled=user.disabled,
is_admin=user.is_admin,
hashed_password=user.hashed_password,
)
return None
@@ -166,12 +167,14 @@ class AuthService:
if exists:
raise HTTPException(status_code=400, detail="用户名已存在")
hashed = cls.get_password_hash(payload.password)
# 第一个用户自动成为超级管理员
user = await UserAccount.create(
username=payload.username,
email=payload.email,
full_name=payload.full_name,
hashed_password=hashed,
disabled=False,
is_admin=True, # 第一个用户是超级管理员
)
return user
@@ -195,6 +198,13 @@ class AuthService:
detail="用户名或密码错误",
headers={"WWW-Authenticate": "Bearer"},
)
# 更新最后登录时间
db_user = await UserAccount.get_or_none(id=user.id)
if db_user:
db_user.last_login = _now()
await db_user.save(update_fields=["last_login"])
access_token_expires = timedelta(minutes=cls.access_token_expire_minutes)
access_token = await cls.create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
@@ -212,6 +222,7 @@ class AuthService:
"email": getattr(user, "email", None),
"full_name": getattr(user, "full_name", None),
"gravatar_url": gravatar_url,
"is_admin": getattr(user, "is_admin", False),
}
@classmethod

View File

@@ -16,6 +16,7 @@ class User(BaseModel):
email: str | None = None
full_name: str | None = None
disabled: bool | None = None
is_admin: bool = False
class UserInDB(User):

View File

@@ -0,0 +1,4 @@
from .service import PermissionService
from .matcher import PathMatcher
__all__ = ["PermissionService", "PathMatcher"]

41
domain/permission/api.py Normal file
View File

@@ -0,0 +1,41 @@
from typing import Annotated
from fastapi import APIRouter, Depends
from domain.auth.service import get_current_active_user
from domain.auth.types import User
from .service import PermissionService
from .types import (
PathPermissionCheck,
PathPermissionResult,
UserPermissions,
PermissionInfo,
)
router = APIRouter(prefix="/api", tags=["permissions"])
@router.get("/permissions", response_model=list[PermissionInfo])
async def get_all_permissions(
current_user: Annotated[User, Depends(get_current_active_user)]
) -> list[PermissionInfo]:
"""获取所有权限定义"""
return await PermissionService.get_all_permissions()
@router.get("/me/permissions", response_model=UserPermissions)
async def get_my_permissions(
current_user: Annotated[User, Depends(get_current_active_user)]
) -> UserPermissions:
"""获取当前用户的有效权限"""
return await PermissionService.get_user_permissions(current_user.id)
@router.post("/me/check-path", response_model=PathPermissionResult)
async def check_path_permission(
data: PathPermissionCheck,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> PathPermissionResult:
"""检查当前用户对某路径的权限"""
return await PermissionService.check_path_permission_detailed(
current_user.id, data.path, data.action
)

View File

@@ -0,0 +1,158 @@
import re
import fnmatch
from functools import lru_cache
class PathMatcher:
"""路径匹配器,支持精确匹配、通配符匹配和正则匹配"""
@classmethod
def normalize_path(cls, path: str) -> str:
"""规范化路径"""
if not path:
return "/"
# 确保以 / 开头
if not path.startswith("/"):
path = "/" + path
# 移除末尾的 /(除了根路径)
if path != "/" and path.endswith("/"):
path = path.rstrip("/")
return path
@classmethod
def get_parent_path(cls, path: str) -> str | None:
"""获取父目录路径"""
path = cls.normalize_path(path)
if path == "/":
return None
parent = "/".join(path.rsplit("/", 1)[:-1])
return parent if parent else "/"
@classmethod
def match_pattern(cls, path: str, pattern: str, is_regex: bool = False) -> bool:
"""
匹配路径和模式
Args:
path: 要匹配的路径
pattern: 匹配模式
is_regex: 是否为正则表达式
Returns:
是否匹配
"""
path = cls.normalize_path(path)
pattern = cls.normalize_path(pattern)
if is_regex:
return cls._match_regex(path, pattern)
else:
return cls._match_glob(path, pattern)
@classmethod
def _match_regex(cls, path: str, pattern: str) -> bool:
"""正则表达式匹配"""
try:
# 限制正则表达式的复杂度,防止 ReDoS 攻击
if len(pattern) > 500:
return False
regex = re.compile(pattern)
return bool(regex.match(path))
except re.error:
return False
@classmethod
def _match_glob(cls, path: str, pattern: str) -> bool:
"""
通配符匹配
支持的语法:
- * : 匹配单层目录中的任意字符
- ** : 匹配任意层级目录
- ? : 匹配单个字符
"""
# 精确匹配
if pattern == path:
return True
# 处理 ** 通配符
if "**" in pattern:
return cls._match_double_star(path, pattern)
# 使用 fnmatch 进行标准通配符匹配
return fnmatch.fnmatch(path, pattern)
@classmethod
def _match_double_star(cls, path: str, pattern: str) -> bool:
"""处理 ** 通配符匹配"""
# 将 ** 替换为特殊标记
parts = pattern.split("**")
if len(parts) == 2:
prefix, suffix = parts
# 移除 prefix 末尾的 / 和 suffix 开头的 /
prefix = prefix.rstrip("/") if prefix else ""
suffix = suffix.lstrip("/") if suffix else ""
# 检查前缀匹配
if prefix and not path.startswith(prefix):
return False
# 如果没有后缀,只需要前缀匹配
if not suffix:
return True
# 检查后缀匹配
remaining = path[len(prefix):].lstrip("/") if prefix else path.lstrip("/")
# 后缀可以出现在任意位置
if "*" in suffix or "?" in suffix:
# 后缀包含通配符,逐层检查
path_parts = remaining.split("/")
suffix_parts = suffix.split("/")
# 简化处理:检查路径的最后几层是否与后缀匹配
if len(path_parts) >= len(suffix_parts):
tail = "/".join(path_parts[-len(suffix_parts):])
return fnmatch.fnmatch(tail, suffix)
return False
else:
# 后缀是精确字符串
return remaining.endswith(suffix) or ("/" + suffix) in remaining or remaining == suffix
# 多个 ** 的情况,使用简化匹配
regex_pattern = pattern.replace("**", ".*").replace("*", "[^/]*").replace("?", ".")
try:
return bool(re.match(f"^{regex_pattern}$", path))
except re.error:
return False
@classmethod
def get_pattern_specificity(cls, pattern: str, is_regex: bool = False) -> int:
"""
计算模式的具体程度(用于优先级排序)
返回值越大表示模式越具体
"""
pattern = cls.normalize_path(pattern)
if is_regex:
# 正则表达式具体程度较低
return len(pattern) // 2
# 精确路径最具体
if "*" not in pattern and "?" not in pattern:
return len(pattern) * 10
# 计算非通配符部分的长度
specificity = 0
parts = pattern.split("/")
for part in parts:
if part == "**":
specificity += 1
elif "*" in part or "?" in part:
specificity += 5
else:
specificity += 10
return specificity

View File

@@ -0,0 +1,351 @@
from typing import List, Optional
from fastapi import HTTPException
from models.database import (
UserAccount,
Role,
UserRole,
Permission,
RolePermission,
PathRule,
)
from .matcher import PathMatcher
from .types import (
PathAction,
PathRuleInfo,
PathPermissionResult,
UserPermissions,
PermissionInfo,
PERMISSION_DEFINITIONS,
)
class PermissionService:
"""权限检查服务"""
# 权限检查结果缓存(简单的内存缓存)
_cache: dict[str, tuple[bool, float]] = {}
_cache_ttl = 300 # 5分钟缓存
@classmethod
async def check_path_permission(
cls, user_id: int, path: str, action: str
) -> bool:
"""
检查用户对路径的操作权限
Args:
user_id: 用户ID
path: 要检查的路径
action: 操作类型 (read/write/delete/share)
Returns:
是否有权限
"""
import time
# 检查缓存
cache_key = f"{user_id}:{path}:{action}"
if cache_key in cls._cache:
result, timestamp = cls._cache[cache_key]
if time.time() - timestamp < cls._cache_ttl:
return result
# 获取用户
user = await UserAccount.get_or_none(id=user_id)
if not user:
return False
# 超级管理员直接放行
if user.is_admin:
cls._cache[cache_key] = (True, time.time())
return True
# 获取用户所有角色
user_roles = await UserRole.filter(user_id=user_id).prefetch_related("role")
role_ids = [ur.role_id for ur in user_roles]
if not role_ids:
cls._cache[cache_key] = (False, time.time())
return False
# 获取所有角色的路径规则
path_rules = await PathRule.filter(role_id__in=role_ids).order_by("-priority")
# 规范化路径
normalized_path = PathMatcher.normalize_path(path)
# 按优先级和具体程度匹配
result = cls._match_path_rules(normalized_path, action, list(path_rules))
# 如果没有匹配到规则,检查父目录(继承)
if result is None:
parent_path = PathMatcher.get_parent_path(normalized_path)
if parent_path:
result = await cls.check_path_permission(user_id, parent_path, action)
else:
result = False # 默认拒绝
cls._cache[cache_key] = (result, time.time())
return result
@classmethod
def _match_path_rules(
cls, path: str, action: str, rules: List[PathRule]
) -> Optional[bool]:
"""
匹配路径规则
Returns:
True/False 表示明确的权限结果None 表示没有匹配到规则
"""
# 按优先级和具体程度排序
sorted_rules = sorted(
rules,
key=lambda r: (
r.priority,
PathMatcher.get_pattern_specificity(r.path_pattern, r.is_regex),
),
reverse=True,
)
for rule in sorted_rules:
if PathMatcher.match_pattern(path, rule.path_pattern, rule.is_regex):
# 匹配到规则,检查具体操作权限
if action == PathAction.READ:
return rule.can_read
elif action == PathAction.WRITE:
return rule.can_write
elif action == PathAction.DELETE:
return rule.can_delete
elif action == PathAction.SHARE:
return rule.can_share
else:
return False
return None
@classmethod
async def check_system_permission(cls, user_id: int, permission_code: str) -> bool:
"""检查用户的系统/适配器权限"""
# 获取用户
user = await UserAccount.get_or_none(id=user_id)
if not user:
return False
# 超级管理员直接放行
if user.is_admin:
return True
# 获取用户所有角色
user_roles = await UserRole.filter(user_id=user_id)
role_ids = [ur.role_id for ur in user_roles]
if not role_ids:
return False
# 检查角色是否有该权限
permission = await Permission.get_or_none(code=permission_code)
if not permission:
return False
role_permission = await RolePermission.filter(
role_id__in=role_ids, permission_id=permission.id
).first()
return role_permission is not None
@classmethod
async def require_path_permission(
cls, user_id: int, path: str, action: str
) -> None:
"""要求用户具有路径权限,否则抛出 403"""
if not await cls.check_path_permission(user_id, path, action):
raise HTTPException(403, detail=f"没有权限执行此操作: {action}")
@classmethod
async def require_system_permission(
cls, user_id: int, permission_code: str
) -> None:
"""要求用户具有系统权限,否则抛出 403"""
if not await cls.check_system_permission(user_id, permission_code):
raise HTTPException(403, detail=f"没有权限: {permission_code}")
@classmethod
async def get_user_permissions(cls, user_id: int) -> UserPermissions:
"""获取用户的所有权限"""
user = await UserAccount.get_or_none(id=user_id)
if not user:
raise HTTPException(404, detail="用户不存在")
# 超级管理员拥有所有权限
if user.is_admin:
all_permissions = await Permission.all()
all_path_rules = await PathRule.all()
return UserPermissions(
user_id=user_id,
is_admin=True,
permissions=[p.code for p in all_permissions],
path_rules=[
PathRuleInfo(
id=r.id,
role_id=r.role_id,
path_pattern=r.path_pattern,
is_regex=r.is_regex,
can_read=r.can_read,
can_write=r.can_write,
can_delete=r.can_delete,
can_share=r.can_share,
priority=r.priority,
created_at=r.created_at,
)
for r in all_path_rules
],
)
# 获取用户角色
user_roles = await UserRole.filter(user_id=user_id)
role_ids = [ur.role_id for ur in user_roles]
# 获取权限
permissions = []
if role_ids:
role_permissions = await RolePermission.filter(
role_id__in=role_ids
).prefetch_related("permission")
permissions = list(set(rp.permission.code for rp in role_permissions))
# 获取路径规则
path_rules = []
if role_ids:
rules = await PathRule.filter(role_id__in=role_ids)
path_rules = [
PathRuleInfo(
id=r.id,
role_id=r.role_id,
path_pattern=r.path_pattern,
is_regex=r.is_regex,
can_read=r.can_read,
can_write=r.can_write,
can_delete=r.can_delete,
can_share=r.can_share,
priority=r.priority,
created_at=r.created_at,
)
for r in rules
]
return UserPermissions(
user_id=user_id,
is_admin=False,
permissions=permissions,
path_rules=path_rules,
)
@classmethod
async def get_all_permissions(cls) -> List[PermissionInfo]:
"""获取所有权限定义"""
permissions = await Permission.all()
return [
PermissionInfo(
id=p.id,
code=p.code,
name=p.name,
category=p.category,
description=p.description,
)
for p in permissions
]
@classmethod
async def check_path_permission_detailed(
cls, user_id: int, path: str, action: str
) -> PathPermissionResult:
"""检查路径权限并返回详细结果"""
user = await UserAccount.get_or_none(id=user_id)
if not user:
return PathPermissionResult(path=path, action=action, allowed=False)
# 超级管理员
if user.is_admin:
return PathPermissionResult(path=path, action=action, allowed=True)
# 获取用户角色
user_roles = await UserRole.filter(user_id=user_id)
role_ids = [ur.role_id for ur in user_roles]
if not role_ids:
return PathPermissionResult(path=path, action=action, allowed=False)
# 获取路径规则
path_rules = await PathRule.filter(role_id__in=role_ids).order_by("-priority")
normalized_path = PathMatcher.normalize_path(path)
# 查找匹配的规则
matched_rule = None
for rule in sorted(
path_rules,
key=lambda r: (
r.priority,
PathMatcher.get_pattern_specificity(r.path_pattern, r.is_regex),
),
reverse=True,
):
if PathMatcher.match_pattern(
normalized_path, rule.path_pattern, rule.is_regex
):
matched_rule = rule
break
# 检查权限
allowed = False
if matched_rule:
if action == PathAction.READ:
allowed = matched_rule.can_read
elif action == PathAction.WRITE:
allowed = matched_rule.can_write
elif action == PathAction.DELETE:
allowed = matched_rule.can_delete
elif action == PathAction.SHARE:
allowed = matched_rule.can_share
rule_info = None
if matched_rule:
rule_info = PathRuleInfo(
id=matched_rule.id,
role_id=matched_rule.role_id,
path_pattern=matched_rule.path_pattern,
is_regex=matched_rule.is_regex,
can_read=matched_rule.can_read,
can_write=matched_rule.can_write,
can_delete=matched_rule.can_delete,
can_share=matched_rule.can_share,
priority=matched_rule.priority,
created_at=matched_rule.created_at,
)
return PathPermissionResult(
path=path, action=action, allowed=allowed, matched_rule=rule_info
)
@classmethod
def clear_cache(cls, user_id: int | None = None) -> None:
"""清除权限缓存"""
if user_id is None:
cls._cache.clear()
else:
# 清除特定用户的缓存
keys_to_delete = [k for k in cls._cache if k.startswith(f"{user_id}:")]
for k in keys_to_delete:
del cls._cache[k]
@classmethod
async def filter_paths_by_permission(
cls, user_id: int, paths: List[str], action: str
) -> List[str]:
"""过滤出用户有权限的路径列表"""
result = []
for path in paths:
if await cls.check_path_permission(user_id, path, action):
result.append(path)
return result

108
domain/permission/types.py Normal file
View File

@@ -0,0 +1,108 @@
from pydantic import BaseModel
from datetime import datetime
# 权限操作类型
class PathAction:
READ = "read"
WRITE = "write"
DELETE = "delete"
SHARE = "share"
# 系统权限代码
class SystemPermission:
USER_CREATE = "system.user.create"
USER_EDIT = "system.user.edit"
USER_DELETE = "system.user.delete"
USER_LIST = "system.user.list"
ROLE_MANAGE = "system.role.manage"
CONFIG_EDIT = "system.config.edit"
AUDIT_VIEW = "system.audit.view"
# 适配器权限代码
class AdapterPermission:
CREATE = "adapter.create"
EDIT = "adapter.edit"
DELETE = "adapter.delete"
LIST = "adapter.list"
# 所有权限定义
PERMISSION_DEFINITIONS = [
# 系统权限
{"code": SystemPermission.USER_CREATE, "name": "创建用户", "category": "system", "description": "允许创建新用户"},
{"code": SystemPermission.USER_EDIT, "name": "编辑用户", "category": "system", "description": "允许编辑用户信息"},
{"code": SystemPermission.USER_DELETE, "name": "删除用户", "category": "system", "description": "允许删除用户"},
{"code": SystemPermission.USER_LIST, "name": "查看用户列表", "category": "system", "description": "允许查看用户列表"},
{"code": SystemPermission.ROLE_MANAGE, "name": "管理角色和权限", "category": "system", "description": "允许管理角色和权限配置"},
{"code": SystemPermission.CONFIG_EDIT, "name": "修改系统配置", "category": "system", "description": "允许修改系统配置"},
{"code": SystemPermission.AUDIT_VIEW, "name": "查看审计日志", "category": "system", "description": "允许查看审计日志"},
# 适配器权限
{"code": AdapterPermission.CREATE, "name": "创建存储适配器", "category": "adapter", "description": "允许创建存储适配器"},
{"code": AdapterPermission.EDIT, "name": "编辑存储适配器", "category": "adapter", "description": "允许编辑存储适配器"},
{"code": AdapterPermission.DELETE, "name": "删除存储适配器", "category": "adapter", "description": "允许删除存储适配器"},
{"code": AdapterPermission.LIST, "name": "查看存储适配器列表", "category": "adapter", "description": "允许查看存储适配器列表"},
]
# Pydantic 模型
class PermissionInfo(BaseModel):
id: int
code: str
name: str
category: str
description: str | None = None
class PathRuleInfo(BaseModel):
id: int
role_id: int
path_pattern: str
is_regex: bool
can_read: bool
can_write: bool
can_delete: bool
can_share: bool
priority: int
created_at: datetime
class PathRuleCreate(BaseModel):
path_pattern: str
is_regex: bool = False
can_read: bool = True
can_write: bool = False
can_delete: bool = False
can_share: bool = False
priority: int = 0
class PathRuleUpdate(BaseModel):
path_pattern: str | None = None
is_regex: bool | None = None
can_read: bool | None = None
can_write: bool | None = None
can_delete: bool | None = None
can_share: bool | None = None
priority: int | None = None
class PathPermissionCheck(BaseModel):
path: str
action: str
class PathPermissionResult(BaseModel):
path: str
action: str
allowed: bool
matched_rule: PathRuleInfo | None = None
class UserPermissions(BaseModel):
user_id: int
is_admin: bool
permissions: list[str] # 系统/适配器权限代码列表
path_rules: list[PathRuleInfo] # 路径权限规则

3
domain/role/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .service import RoleService
__all__ = ["RoleService"]

142
domain/role/api.py Normal file
View File

@@ -0,0 +1,142 @@
from typing import Annotated
from fastapi import APIRouter, Depends
from domain.auth.service import get_current_active_user
from domain.auth.types import User
from domain.permission.service import PermissionService
from domain.permission.types import PathRuleCreate, PathRuleInfo, SystemPermission
from domain.user.service import UserService
from domain.user.types import UserInfo
from .service import RoleService
from .types import RoleCreate, RoleDetail, RoleInfo, RolePermissionsUpdate, RoleUpdate
router = APIRouter(prefix="/api", tags=["role"])
@router.get("/roles", response_model=list[RoleInfo])
async def list_roles(
current_user: Annotated[User, Depends(get_current_active_user)]
) -> list[RoleInfo]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.get_all_roles()
@router.get("/roles/{role_id}", response_model=RoleDetail)
async def get_role(
role_id: int,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> RoleDetail:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.get_role(role_id)
@router.get("/roles/{role_id}/users", response_model=list[UserInfo])
async def list_role_users(
role_id: int,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> list[UserInfo]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await UserService.get_users_by_role(role_id)
@router.post("/roles", response_model=RoleInfo)
async def create_role(
data: RoleCreate,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> RoleInfo:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.create_role(data)
@router.put("/roles/{role_id}", response_model=RoleInfo)
async def update_role(
role_id: int,
data: RoleUpdate,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> RoleInfo:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.update_role(role_id, data)
@router.delete("/roles/{role_id}")
async def delete_role(
role_id: int,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> dict:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
await RoleService.delete_role(role_id)
return {"success": True}
@router.post("/roles/{role_id}/permissions", response_model=list[str])
async def set_role_permissions(
role_id: int,
data: RolePermissionsUpdate,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> list[str]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.set_role_permissions(role_id, data.permission_codes)
@router.get("/roles/{role_id}/path-rules", response_model=list[PathRuleInfo])
async def get_role_path_rules(
role_id: int,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> list[PathRuleInfo]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.get_role_path_rules(role_id)
@router.post("/roles/{role_id}/path-rules", response_model=PathRuleInfo)
async def add_path_rule(
role_id: int,
data: PathRuleCreate,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> PathRuleInfo:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.add_path_rule(role_id, data)
@router.put("/path-rules/{rule_id}", response_model=PathRuleInfo)
async def update_path_rule(
rule_id: int,
data: PathRuleCreate,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> PathRuleInfo:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.update_path_rule(rule_id, data)
@router.delete("/path-rules/{rule_id}")
async def delete_path_rule(
rule_id: int,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> dict:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
await RoleService.delete_path_rule(rule_id)
return {"success": True}

327
domain/role/service.py Normal file
View File

@@ -0,0 +1,327 @@
from typing import List
from fastapi import HTTPException
from models.database import Role, RolePermission, Permission, PathRule, UserRole
from domain.permission.service import PermissionService
from domain.permission.types import PathRuleCreate, PathRuleInfo
from .types import RoleInfo, RoleDetail, RoleCreate, RoleUpdate, SystemRoles
class RoleService:
"""角色管理服务"""
@classmethod
async def get_all_roles(cls) -> List[RoleInfo]:
"""获取所有角色"""
roles = await Role.all().order_by("id")
return [
RoleInfo(
id=r.id,
name=r.name,
description=r.description,
is_system=r.is_system,
created_at=r.created_at,
)
for r in roles
]
@classmethod
async def get_role(cls, role_id: int) -> RoleDetail:
"""获取角色详情"""
role = await Role.get_or_none(id=role_id)
if not role:
raise HTTPException(404, detail="角色不存在")
# 获取权限
role_permissions = await RolePermission.filter(role_id=role_id).prefetch_related(
"permission"
)
permissions = [rp.permission.code for rp in role_permissions]
# 获取路径规则数量
path_rules_count = await PathRule.filter(role_id=role_id).count()
return RoleDetail(
id=role.id,
name=role.name,
description=role.description,
is_system=role.is_system,
created_at=role.created_at,
permissions=permissions,
path_rules_count=path_rules_count,
)
@classmethod
async def create_role(cls, data: RoleCreate) -> RoleInfo:
"""创建角色"""
# 检查名称是否已存在
existing = await Role.get_or_none(name=data.name)
if existing:
raise HTTPException(400, detail="角色名称已存在")
role = await Role.create(
name=data.name,
description=data.description,
is_system=False,
)
return RoleInfo(
id=role.id,
name=role.name,
description=role.description,
is_system=role.is_system,
created_at=role.created_at,
)
@classmethod
async def update_role(cls, role_id: int, data: RoleUpdate) -> RoleInfo:
"""更新角色"""
role = await Role.get_or_none(id=role_id)
if not role:
raise HTTPException(404, detail="角色不存在")
if data.name is not None:
# 检查名称是否与其他角色冲突
existing = await Role.filter(name=data.name).exclude(id=role_id).first()
if existing:
raise HTTPException(400, detail="角色名称已存在")
role.name = data.name
if data.description is not None:
role.description = data.description
await role.save()
return RoleInfo(
id=role.id,
name=role.name,
description=role.description,
is_system=role.is_system,
created_at=role.created_at,
)
@classmethod
async def delete_role(cls, role_id: int) -> None:
"""删除角色"""
role = await Role.get_or_none(id=role_id)
if not role:
raise HTTPException(404, detail="角色不存在")
if role.is_system:
raise HTTPException(400, detail="系统内置角色不可删除")
# 检查是否有用户使用此角色
user_count = await UserRole.filter(role_id=role_id).count()
if user_count > 0:
raise HTTPException(400, detail=f"{user_count} 个用户正在使用此角色,无法删除")
await role.delete()
# 清除权限缓存
PermissionService.clear_cache()
@classmethod
async def set_role_permissions(cls, role_id: int, permission_codes: List[str]) -> List[str]:
"""设置角色的权限"""
role = await Role.get_or_none(id=role_id)
if not role:
raise HTTPException(404, detail="角色不存在")
# 获取权限ID
permissions = await Permission.filter(code__in=permission_codes)
permission_map = {p.code: p.id for p in permissions}
# 验证所有权限代码
invalid_codes = set(permission_codes) - set(permission_map.keys())
if invalid_codes:
raise HTTPException(400, detail=f"无效的权限代码: {', '.join(invalid_codes)}")
# 删除现有权限
await RolePermission.filter(role_id=role_id).delete()
# 添加新权限
for code in permission_codes:
await RolePermission.create(
role_id=role_id,
permission_id=permission_map[code],
)
# 清除权限缓存
PermissionService.clear_cache()
return permission_codes
@classmethod
async def get_role_path_rules(cls, role_id: int) -> List[PathRuleInfo]:
"""获取角色的路径规则"""
role = await Role.get_or_none(id=role_id)
if not role:
raise HTTPException(404, detail="角色不存在")
rules = await PathRule.filter(role_id=role_id).order_by("-priority", "id")
return [
PathRuleInfo(
id=r.id,
role_id=r.role_id,
path_pattern=r.path_pattern,
is_regex=r.is_regex,
can_read=r.can_read,
can_write=r.can_write,
can_delete=r.can_delete,
can_share=r.can_share,
priority=r.priority,
created_at=r.created_at,
)
for r in rules
]
@classmethod
async def add_path_rule(cls, role_id: int, data: PathRuleCreate) -> PathRuleInfo:
"""添加路径规则"""
role = await Role.get_or_none(id=role_id)
if not role:
raise HTTPException(404, detail="角色不存在")
# 验证路径模式
if data.is_regex:
import re
try:
re.compile(data.path_pattern)
except re.error as e:
raise HTTPException(400, detail=f"无效的正则表达式: {e}")
rule = await PathRule.create(
role_id=role_id,
path_pattern=data.path_pattern,
is_regex=data.is_regex,
can_read=data.can_read,
can_write=data.can_write,
can_delete=data.can_delete,
can_share=data.can_share,
priority=data.priority,
)
# 清除权限缓存
PermissionService.clear_cache()
return PathRuleInfo(
id=rule.id,
role_id=rule.role_id,
path_pattern=rule.path_pattern,
is_regex=rule.is_regex,
can_read=rule.can_read,
can_write=rule.can_write,
can_delete=rule.can_delete,
can_share=rule.can_share,
priority=rule.priority,
created_at=rule.created_at,
)
@classmethod
async def update_path_rule(cls, rule_id: int, data: PathRuleCreate) -> PathRuleInfo:
"""更新路径规则"""
rule = await PathRule.get_or_none(id=rule_id)
if not rule:
raise HTTPException(404, detail="路径规则不存在")
# 验证路径模式
if data.is_regex:
import re
try:
re.compile(data.path_pattern)
except re.error as e:
raise HTTPException(400, detail=f"无效的正则表达式: {e}")
rule.path_pattern = data.path_pattern
rule.is_regex = data.is_regex
rule.can_read = data.can_read
rule.can_write = data.can_write
rule.can_delete = data.can_delete
rule.can_share = data.can_share
rule.priority = data.priority
await rule.save()
# 清除权限缓存
PermissionService.clear_cache()
return PathRuleInfo(
id=rule.id,
role_id=rule.role_id,
path_pattern=rule.path_pattern,
is_regex=rule.is_regex,
can_read=rule.can_read,
can_write=rule.can_write,
can_delete=rule.can_delete,
can_share=rule.can_share,
priority=rule.priority,
created_at=rule.created_at,
)
@classmethod
async def delete_path_rule(cls, rule_id: int) -> None:
"""删除路径规则"""
rule = await PathRule.get_or_none(id=rule_id)
if not rule:
raise HTTPException(404, detail="路径规则不存在")
await rule.delete()
# 清除权限缓存
PermissionService.clear_cache()
@classmethod
async def ensure_system_roles(cls) -> None:
"""确保系统内置角色存在"""
system_roles = [
{
"name": SystemRoles.ADMIN,
"description": "管理员角色,拥有所有系统和适配器权限",
"is_system": True,
},
{
"name": SystemRoles.USER,
"description": "普通用户角色,需要管理员配置路径权限",
"is_system": True,
},
{
"name": SystemRoles.VIEWER,
"description": "只读用户角色,仅可查看文件",
"is_system": True,
},
]
for role_data in system_roles:
existing = await Role.get_or_none(name=role_data["name"])
if not existing:
await Role.create(**role_data)
@classmethod
async def setup_admin_role_permissions(cls) -> None:
"""为管理员角色设置所有权限"""
admin_role = await Role.get_or_none(name=SystemRoles.ADMIN)
if not admin_role:
return
# 获取所有权限
all_permissions = await Permission.all()
# 清除现有权限
await RolePermission.filter(role_id=admin_role.id).delete()
# 添加所有权限
for perm in all_permissions:
await RolePermission.create(role_id=admin_role.id, permission_id=perm.id)
# 添加全路径访问规则
existing_rule = await PathRule.filter(
role_id=admin_role.id, path_pattern="/**"
).first()
if not existing_rule:
await PathRule.create(
role_id=admin_role.id,
path_pattern="/**",
is_regex=False,
can_read=True,
can_write=True,
can_delete=True,
can_share=True,
priority=100,
)

36
domain/role/types.py Normal file
View File

@@ -0,0 +1,36 @@
from pydantic import BaseModel
from datetime import datetime
class RoleInfo(BaseModel):
id: int
name: str
description: str | None = None
is_system: bool
created_at: datetime
class RoleDetail(RoleInfo):
permissions: list[str] # 权限代码列表
path_rules_count: int
class RoleCreate(BaseModel):
name: str
description: str | None = None
class RoleUpdate(BaseModel):
name: str | None = None
description: str | None = None
class RolePermissionsUpdate(BaseModel):
permission_codes: list[str]
# 预置角色名称
class SystemRoles:
ADMIN = "Admin"
USER = "User"
VIEWER = "Viewer"

4
domain/user/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
from .service import UserService
__all__ = ["UserService"]

94
domain/user/api.py Normal file
View File

@@ -0,0 +1,94 @@
from typing import Annotated
from fastapi import APIRouter, Depends
from domain.auth.service import get_current_active_user
from domain.auth.types import User
from domain.permission.service import PermissionService
from domain.permission.types import SystemPermission
from .service import UserService
from .types import UserCreate, UserDetail, UserInfo, UserRoleAssign, UserUpdate
router = APIRouter(prefix="/api", tags=["user"])
@router.get("/users", response_model=list[UserInfo])
async def list_users(
current_user: Annotated[User, Depends(get_current_active_user)]
) -> list[UserInfo]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_LIST
)
return await UserService.get_all_users()
@router.get("/users/{user_id}", response_model=UserDetail)
async def get_user(
user_id: int,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> UserDetail:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_LIST
)
return await UserService.get_user(user_id)
@router.post("/users", response_model=UserDetail)
async def create_user(
data: UserCreate,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> UserDetail:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_CREATE
)
return await UserService.create_user(data, current_user.id)
@router.put("/users/{user_id}", response_model=UserDetail)
async def update_user(
user_id: int,
data: UserUpdate,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> UserDetail:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_EDIT
)
return await UserService.update_user(user_id, data, current_user.id)
@router.delete("/users/{user_id}")
async def delete_user(
user_id: int,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> dict:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_DELETE
)
await UserService.delete_user(user_id, current_user.id)
return {"success": True}
@router.post("/users/{user_id}/roles", response_model=list[str])
async def set_user_roles(
user_id: int,
data: UserRoleAssign,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> list[str]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_EDIT
)
return await UserService.set_user_roles(user_id, data.role_ids)
@router.delete("/users/{user_id}/roles/{role_id}", response_model=list[str])
async def remove_user_role(
user_id: int,
role_id: int,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> list[str]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_EDIT
)
return await UserService.remove_user_role(user_id, role_id)

190
domain/user/service.py Normal file
View File

@@ -0,0 +1,190 @@
from typing import List
from fastapi import HTTPException
from domain.auth.service import AuthService
from domain.permission.service import PermissionService
from models.database import Role, UserAccount, UserRole
from .types import UserCreate, UserDetail, UserInfo, UserUpdate
class UserService:
"""用户管理服务"""
@classmethod
async def get_all_users(cls) -> List[UserInfo]:
users = await UserAccount.all().order_by("id")
return [
UserInfo(
id=u.id,
username=u.username,
email=u.email,
full_name=u.full_name,
disabled=u.disabled,
is_admin=u.is_admin,
created_at=u.created_at,
last_login=u.last_login,
)
for u in users
]
@classmethod
async def get_user(cls, user_id: int) -> UserDetail:
user = await UserAccount.get_or_none(id=user_id).prefetch_related("created_by")
if not user:
raise HTTPException(404, detail="用户不存在")
user_roles = await UserRole.filter(user_id=user_id).prefetch_related("role")
roles = [ur.role.name for ur in user_roles]
created_by_username = None
if user.created_by_id:
creator = await UserAccount.get_or_none(id=user.created_by_id)
if creator:
created_by_username = creator.username
return UserDetail(
id=user.id,
username=user.username,
email=user.email,
full_name=user.full_name,
disabled=user.disabled,
is_admin=user.is_admin,
created_at=user.created_at,
last_login=user.last_login,
roles=roles,
created_by_username=created_by_username,
)
@classmethod
async def get_users_by_role(cls, role_id: int) -> List[UserInfo]:
role = await Role.get_or_none(id=role_id)
if not role:
raise HTTPException(404, detail="角色不存在")
user_roles = await UserRole.filter(role_id=role_id).prefetch_related("user")
users = [ur.user for ur in user_roles if ur.user]
users.sort(key=lambda u: u.id)
return [
UserInfo(
id=u.id,
username=u.username,
email=u.email,
full_name=u.full_name,
disabled=u.disabled,
is_admin=u.is_admin,
created_at=u.created_at,
last_login=u.last_login,
)
for u in users
]
@classmethod
async def create_user(cls, data: UserCreate, creator_id: int) -> UserDetail:
existing = await UserAccount.get_or_none(username=data.username)
if existing:
raise HTTPException(400, detail="用户名已存在")
if data.email:
existing_email = await UserAccount.get_or_none(email=data.email)
if existing_email:
raise HTTPException(400, detail="邮箱已被使用")
hashed_password = AuthService.get_password_hash(data.password)
user = await UserAccount.create(
username=data.username,
email=data.email,
full_name=data.full_name,
hashed_password=hashed_password,
disabled=data.disabled,
is_admin=data.is_admin,
created_by_id=creator_id,
)
if data.role_ids:
for role_id in data.role_ids:
role = await Role.get_or_none(id=role_id)
if role:
await UserRole.create(user_id=user.id, role_id=role_id)
return await cls.get_user(user.id)
@classmethod
async def update_user(cls, user_id: int, data: UserUpdate, operator_id: int) -> UserDetail:
user = await UserAccount.get_or_none(id=user_id)
if not user:
raise HTTPException(404, detail="用户不存在")
if data.is_admin is not None and user_id == operator_id:
raise HTTPException(400, detail="不能修改自己的管理员状态")
if data.email is not None:
existing = await UserAccount.filter(email=data.email).exclude(id=user_id).first()
if existing:
raise HTTPException(400, detail="邮箱已被使用")
user.email = data.email
if data.full_name is not None:
user.full_name = data.full_name
if data.password is not None:
user.hashed_password = AuthService.get_password_hash(data.password)
if data.is_admin is not None:
user.is_admin = data.is_admin
if data.disabled is not None:
if user_id == operator_id and data.disabled:
raise HTTPException(400, detail="不能禁用自己")
user.disabled = data.disabled
await user.save()
PermissionService.clear_cache(user_id)
return await cls.get_user(user_id)
@classmethod
async def delete_user(cls, user_id: int, operator_id: int) -> None:
if user_id == operator_id:
raise HTTPException(400, detail="不能删除自己")
user = await UserAccount.get_or_none(id=user_id)
if not user:
raise HTTPException(404, detail="用户不存在")
await UserRole.filter(user_id=user_id).delete()
await user.delete()
PermissionService.clear_cache(user_id)
@classmethod
async def set_user_roles(cls, user_id: int, role_ids: List[int]) -> List[str]:
user = await UserAccount.get_or_none(id=user_id)
if not user:
raise HTTPException(404, detail="用户不存在")
roles = await Role.filter(id__in=role_ids)
valid_role_ids = {r.id for r in roles}
invalid_ids = set(role_ids) - valid_role_ids
if invalid_ids:
raise HTTPException(400, detail=f"无效的角色ID: {invalid_ids}")
await UserRole.filter(user_id=user_id).delete()
for role_id in role_ids:
await UserRole.create(user_id=user_id, role_id=role_id)
PermissionService.clear_cache(user_id)
return [r.name for r in roles if r.id in role_ids]
@classmethod
async def remove_user_role(cls, user_id: int, role_id: int) -> List[str]:
user = await UserAccount.get_or_none(id=user_id)
if not user:
raise HTTPException(404, detail="用户不存在")
await UserRole.filter(user_id=user_id, role_id=role_id).delete()
PermissionService.clear_cache(user_id)
user_roles = await UserRole.filter(user_id=user_id).prefetch_related("role")
return [ur.role.name for ur in user_roles]

42
domain/user/types.py Normal file
View File

@@ -0,0 +1,42 @@
from datetime import datetime
from pydantic import BaseModel
class UserInfo(BaseModel):
id: int
username: str
email: str | None = None
full_name: str | None = None
disabled: bool
is_admin: bool
created_at: datetime
last_login: datetime | None = None
class UserDetail(UserInfo):
roles: list[str]
created_by_username: str | None = None
class UserCreate(BaseModel):
username: str
password: str
email: str | None = None
full_name: str | None = None
is_admin: bool = False
disabled: bool = False
role_ids: list[int] = []
class UserUpdate(BaseModel):
email: str | None = None
full_name: str | None = None
password: str | None = None
is_admin: bool | None = None
disabled: bool | None = None
class UserRoleAssign(BaseModel):
role_ids: list[int]

View File

@@ -5,6 +5,8 @@ from fastapi import APIRouter, Depends, File, Query, Request, UploadFile
from api.response import success
from domain.audit import AuditAction, audit
from domain.auth import User, get_current_active_user
from domain.permission.service import PermissionService
from domain.permission.types import PathAction
from .service import VirtualFSService
from .types import MkdirRequest, MoveRequest
@@ -18,6 +20,7 @@ async def get_file(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
):
await PermissionService.require_path_permission(current_user.id, full_path, PathAction.READ)
return await VirtualFSService.serve_file(full_path, request.headers.get("Range"))
@@ -50,6 +53,7 @@ async def get_temp_link(
current_user: Annotated[User, Depends(get_current_active_user)],
expires_in: int = Query(3600, description="有效时间(秒), 0或负数表示永久"),
):
await PermissionService.require_path_permission(current_user.id, full_path, PathAction.SHARE)
data = await VirtualFSService.create_temp_link(full_path, expires_in)
return success(data)
@@ -80,6 +84,7 @@ async def get_file_stat(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
):
await PermissionService.require_path_permission(current_user.id, full_path, PathAction.READ)
stat = await VirtualFSService.stat(full_path)
return success(stat)
@@ -92,6 +97,7 @@ async def put_file(
full_path: str,
file: UploadFile = File(...),
):
await PermissionService.require_path_permission(current_user.id, full_path, PathAction.WRITE)
data = await file.read()
result = await VirtualFSService.write_uploaded_file(full_path, data)
return success(result)
@@ -104,6 +110,7 @@ async def api_mkdir(
current_user: Annotated[User, Depends(get_current_active_user)],
body: MkdirRequest,
):
await PermissionService.require_path_permission(current_user.id, body.path, PathAction.WRITE)
result = await VirtualFSService.mkdir(body.path)
return success(result)
@@ -116,6 +123,9 @@ async def api_move(
body: MoveRequest,
overwrite: bool = Query(False, description="是否允许覆盖已存在目标"),
):
# 移动需要源路径的删除权限和目标路径的写权限
await PermissionService.require_path_permission(current_user.id, body.src, PathAction.DELETE)
await PermissionService.require_path_permission(current_user.id, body.dst, PathAction.WRITE)
result = await VirtualFSService.move(body.src, body.dst, overwrite)
return success(result)
@@ -128,6 +138,8 @@ async def api_rename(
body: MoveRequest,
overwrite: bool = Query(False, description="是否允许覆盖已存在目标"),
):
# 重命名需要写权限
await PermissionService.require_path_permission(current_user.id, body.src, PathAction.WRITE)
result = await VirtualFSService.rename(body.src, body.dst, overwrite)
return success(result)
@@ -140,6 +152,9 @@ async def api_copy(
body: MoveRequest,
overwrite: bool = Query(False, description="是否覆盖已存在目标"),
):
# 复制需要源路径的读权限和目标路径的写权限
await PermissionService.require_path_permission(current_user.id, body.src, PathAction.READ)
await PermissionService.require_path_permission(current_user.id, body.dst, PathAction.WRITE)
result = await VirtualFSService.copy(body.src, body.dst, overwrite)
return success(result)
@@ -154,6 +169,7 @@ async def upload_stream(
overwrite: bool = Query(True, description="是否覆盖已存在文件"),
chunk_size: int = Query(1024 * 1024, ge=8 * 1024, le=8 * 1024 * 1024, description="单次读取块大小"),
):
await PermissionService.require_path_permission(current_user.id, full_path, PathAction.WRITE)
result = await VirtualFSService.upload_stream_from_upload_file(full_path, file, chunk_size, overwrite)
return success(result)
@@ -169,7 +185,10 @@ async def browse_fs(
sort_by: str = Query("name", description="按字段排序: name, size, mtime"),
sort_order: str = Query("asc", description="排序顺序: asc, desc"),
):
data = await VirtualFSService.list_directory(full_path, page_num, page_size, sort_by, sort_order)
await PermissionService.require_path_permission(current_user.id, full_path, PathAction.READ)
data = await VirtualFSService.list_directory_with_permission(
full_path, current_user.id, page_num, page_size, sort_by, sort_order
)
return success(data)
@@ -180,6 +199,7 @@ async def api_delete(
current_user: Annotated[User, Depends(get_current_active_user)],
full_path: str,
):
await PermissionService.require_path_permission(current_user.id, full_path, PathAction.DELETE)
result = await VirtualFSService.delete(full_path)
return success(result)
@@ -194,5 +214,8 @@ async def root_listing(
sort_by: str = Query("name", description="按字段排序: name, size, mtime"),
sort_order: str = Query("asc", description="排序顺序: asc, desc"),
):
data = await VirtualFSService.list_directory("/", page_num, page_size, sort_by, sort_order)
# 根目录不需要权限检查,但需要过滤无权限的子目录
data = await VirtualFSService.list_directory_with_permission(
"/", current_user.id, page_num, page_size, sort_by, sort_order
)
return success(data)

View File

@@ -5,6 +5,8 @@ from fastapi import HTTPException
from api.response import page
from domain.adapters import runtime_registry
from domain.ai import FILE_COLLECTION_NAME, VECTOR_COLLECTION_NAME, VectorDBService
from domain.permission.service import PermissionService
from domain.permission.types import PathAction
from .thumbnail import is_image_filename, is_video_filename
from models import StorageAdapter
@@ -245,3 +247,54 @@ class VirtualFSListingMixin(VirtualFSResolverMixin):
info["vector_index"] = vector_index
return info
@classmethod
async def list_virtual_dir_with_permission(
cls,
path: str,
user_id: int,
page_num: int = 1,
page_size: int = 50,
sort_by: str = "name",
sort_order: str = "asc",
) -> Dict:
"""
带权限过滤的目录列表
过滤掉用户没有读取权限的条目
"""
# 首先获取完整的目录列表
result = await cls.list_virtual_dir(path, page_num, page_size, sort_by, sort_order)
# 检查用户是否是管理员(管理员可以看到所有内容)
from models.database import UserAccount
user = await UserAccount.get_or_none(id=user_id)
if user and user.is_admin:
return result
# 过滤无权限的条目
items = result.get("items", [])
if not items:
return result
norm = cls._normalize_path(path).rstrip("/") or "/"
filtered_items = []
for item in items:
item_name = item.get("name", "")
if norm == "/":
item_path = f"/{item_name}"
else:
item_path = f"{norm}/{item_name}"
# 检查用户是否有读取权限
has_permission = await PermissionService.check_path_permission(
user_id, item_path, PathAction.READ
)
if has_permission:
filtered_items.append(item)
# 更新结果
result["items"] = filtered_items
return result

View File

@@ -18,4 +18,40 @@ class VirtualFSService(
VirtualFSResolverMixin,
VirtualFSCommonMixin,
):
pass
@classmethod
async def list_directory(
cls,
path: str,
page_num: int = 1,
page_size: int = 50,
sort_by: str = "name",
sort_order: str = "asc",
):
"""列出目录内容"""
return await cls.list_virtual_dir(path, page_num, page_size, sort_by, sort_order)
@classmethod
async def list_directory_with_permission(
cls,
path: str,
user_id: int,
page_num: int = 1,
page_size: int = 50,
sort_by: str = "name",
sort_order: str = "asc",
):
"""列出目录内容(带权限过滤)"""
full_path = cls._normalize_path(path).rstrip("/") or "/"
result = await cls.list_virtual_dir_with_permission(
full_path, user_id, page_num, page_size, sort_by, sort_order
)
return {
"path": full_path,
"entries": result.get("items", []) if isinstance(result, dict) else [],
"pagination": {
"total": result.get("total", 0) if isinstance(result, dict) else 0,
"page": result.get("page", page_num) if isinstance(result, dict) else page_num,
"page_size": result.get("page_size", page_size) if isinstance(result, dict) else page_size,
"pages": result.get("pages", 0) if isinstance(result, dict) else 0,
},
}