diff --git a/.gitignore b/.gitignore index 9368ade..19ca86c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .venv/ __pycache__/ +*/__pycache__ *.pyc # 运行时数据 diff --git a/web/backend/__pycache__/database.cpython-312.pyc b/web/backend/__pycache__/database.cpython-312.pyc index 537e6ee..562d44a 100644 Binary files a/web/backend/__pycache__/database.cpython-312.pyc and b/web/backend/__pycache__/database.cpython-312.pyc differ diff --git a/web/backend/__pycache__/deps.cpython-312.pyc b/web/backend/__pycache__/deps.cpython-312.pyc index 2563ff6..fae3d62 100644 Binary files a/web/backend/__pycache__/deps.cpython-312.pyc and b/web/backend/__pycache__/deps.cpython-312.pyc differ diff --git a/web/backend/__pycache__/models.cpython-312.pyc b/web/backend/__pycache__/models.cpython-312.pyc index 90d7e0b..1f10992 100644 Binary files a/web/backend/__pycache__/models.cpython-312.pyc and b/web/backend/__pycache__/models.cpython-312.pyc differ diff --git a/web/backend/__pycache__/permissions.cpython-312.pyc b/web/backend/__pycache__/permissions.cpython-312.pyc index c92afda..5375917 100644 Binary files a/web/backend/__pycache__/permissions.cpython-312.pyc and b/web/backend/__pycache__/permissions.cpython-312.pyc differ diff --git a/web/backend/__pycache__/schemas.cpython-312.pyc b/web/backend/__pycache__/schemas.cpython-312.pyc index 1dcce47..0239c89 100644 Binary files a/web/backend/__pycache__/schemas.cpython-312.pyc and b/web/backend/__pycache__/schemas.cpython-312.pyc differ diff --git a/web/backend/database.py b/web/backend/database.py index 2cdaa23..8bd0a3a 100644 --- a/web/backend/database.py +++ b/web/backend/database.py @@ -44,6 +44,13 @@ def _migrate(): conn.execute(text("ALTER TABLE accounts ADD COLUMN tag VARCHAR(64) DEFAULT ''")) conn.commit() + # 检查 users.custom_permissions 列是否存在 + result = conn.execute(text("PRAGMA table_info(users)")) + columns = [row[1] for row in result] + if 'custom_permissions' not in columns: + conn.execute(text("ALTER TABLE users ADD COLUMN custom_permissions JSON DEFAULT NULL")) + conn.commit() + def _seed(): """写入默认超管账号和角色。""" diff --git a/web/backend/deps.py b/web/backend/deps.py index 91f856a..6498b9d 100644 --- a/web/backend/deps.py +++ b/web/backend/deps.py @@ -8,7 +8,7 @@ from jose import JWTError from .database import get_db from .security import decode_access_token from .models import User -from .permissions import get_role_permissions +from .permissions import get_user_permissions oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") @@ -43,7 +43,7 @@ def require_permission(permission: str): def checker(current_user: User = Depends(get_current_user)) -> User: if not current_user.is_active: raise HTTPException(status_code=403, detail="账号已禁用") - perms = get_role_permissions(current_user.role) + perms = get_user_permissions(current_user) if permission not in perms: raise HTTPException(status_code=403, detail=f"无权限: {permission}") return current_user diff --git a/web/backend/models.py b/web/backend/models.py index 95ee8ef..188a53c 100644 --- a/web/backend/models.py +++ b/web/backend/models.py @@ -18,6 +18,7 @@ class User(Base): role = Column(String(32), nullable=False, default="support") # super_admin / operation / support is_active = Column(Boolean, default=True) remark = Column(String(256), default="") + custom_permissions = Column(JSON, nullable=True, comment="自定义权限列表,null表示使用角色默认权限") created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) diff --git a/web/backend/permissions.py b/web/backend/permissions.py index 686cac1..0c44b91 100644 --- a/web/backend/permissions.py +++ b/web/backend/permissions.py @@ -48,7 +48,6 @@ ROLE_PERMISSIONS = { ], "support": [ "account:view_assigned", - "login:view_assigned", ], } @@ -65,6 +64,16 @@ def get_role_permissions(role: str) -> list[str]: return ROLE_PERMISSIONS.get(role, []) +def get_user_permissions(user) -> list[str]: + """获取用户的实际权限列表。 + 优先使用用户自定义权限(custom_permissions), + 若为 None 则回退到角色默认权限。 + """ + if user.custom_permissions is not None: + return user.custom_permissions + return get_role_permissions(user.role) + + def has_permission(role: str, permission: str) -> bool: """检查角色是否拥有某权限。""" return permission in get_role_permissions(role) diff --git a/web/backend/routers/__pycache__/auth.cpython-312.pyc b/web/backend/routers/__pycache__/auth.cpython-312.pyc index c7b98e2..63b3864 100644 Binary files a/web/backend/routers/__pycache__/auth.cpython-312.pyc and b/web/backend/routers/__pycache__/auth.cpython-312.pyc differ diff --git a/web/backend/routers/__pycache__/users.cpython-312.pyc b/web/backend/routers/__pycache__/users.cpython-312.pyc index 59250bf..109c854 100644 Binary files a/web/backend/routers/__pycache__/users.cpython-312.pyc and b/web/backend/routers/__pycache__/users.cpython-312.pyc differ diff --git a/web/backend/routers/auth.py b/web/backend/routers/auth.py index 81d2bc6..c912cd9 100644 --- a/web/backend/routers/auth.py +++ b/web/backend/routers/auth.py @@ -7,7 +7,7 @@ from sqlalchemy.orm import Session from ..database import get_db from ..models import User, AuditLog from ..security import verify_password, create_access_token -from ..permissions import get_role_permissions, ROLE_LABELS +from ..permissions import get_user_permissions, ROLE_LABELS from ..schemas import LoginRequest, TokenResponse from ..deps import get_current_user @@ -23,7 +23,7 @@ def login(req: LoginRequest, db: Session = Depends(get_db)): raise HTTPException(status_code=403, detail="账号已禁用,请联系管理员") token = create_access_token({"sub": str(user.id), "role": user.role}) - perms = get_role_permissions(user.role) + perms = get_user_permissions(user) # 审计 db.add(AuditLog(user_id=user.id, username=user.username, action="login", target="auth")) @@ -46,7 +46,7 @@ def me(current_user: User = Depends(get_current_user)): "role_label": ROLE_LABELS.get(current_user.role, current_user.role), "is_active": current_user.is_active, "remark": current_user.remark, - "permissions": get_role_permissions(current_user.role), + "permissions": get_user_permissions(current_user), } diff --git a/web/backend/routers/users.py b/web/backend/routers/users.py index 2cda1dc..55f235e 100644 --- a/web/backend/routers/users.py +++ b/web/backend/routers/users.py @@ -6,7 +6,7 @@ from sqlalchemy.orm import Session from ..database import get_db from ..models import User, AuditLog from ..security import hash_password -from ..permissions import ROLE_LABELS, get_role_permissions +from ..permissions import ROLE_LABELS, PERMISSIONS, get_user_permissions from ..schemas import UserCreate, UserUpdate, UserInfo from ..deps import require_permission, get_current_user @@ -28,7 +28,8 @@ def list_users( is_active=u.is_active, remark=u.remark or "", created_at=u.created_at, - permissions=get_role_permissions(u.role), + permissions=get_user_permissions(u), + custom_permissions=u.custom_permissions, )) return result @@ -60,7 +61,8 @@ def create_user( return UserInfo( id=user.id, username=user.username, role=user.role, is_active=user.is_active, remark=user.remark or "", - permissions=get_role_permissions(user.role), + permissions=get_user_permissions(user), + custom_permissions=user.custom_permissions, ) @@ -83,6 +85,8 @@ def update_user( user.is_active = req.is_active if req.remark is not None: user.remark = req.remark + if req.custom_permissions is not None: + user.custom_permissions = req.custom_permissions db.commit() db.refresh(user) @@ -94,7 +98,8 @@ def update_user( return UserInfo( id=user.id, username=user.username, role=user.role, is_active=user.is_active, remark=user.remark or "", - permissions=get_role_permissions(user.role), + permissions=get_user_permissions(user), + custom_permissions=user.custom_permissions, ) @@ -115,3 +120,13 @@ def delete_user( db.delete(user) db.commit() return {"message": "已删除", "success": True} + + +@router.get("/permissions/list") +def list_permissions(_: User = Depends(require_permission("user:assign_permissions"))): + """返回所有可用权限点及角色默认权限映射。""" + from ..permissions import ROLE_PERMISSIONS + return { + "permissions": PERMISSIONS, + "role_permissions": ROLE_PERMISSIONS, + } diff --git a/web/backend/schemas.py b/web/backend/schemas.py index 1edaed3..e713aa9 100644 --- a/web/backend/schemas.py +++ b/web/backend/schemas.py @@ -27,6 +27,7 @@ class UserInfo(BaseModel): remark: str = "" created_at: Optional[datetime] = None permissions: list[str] = [] + custom_permissions: Optional[list[str]] = None class Config: from_attributes = True @@ -45,6 +46,7 @@ class UserUpdate(BaseModel): role: Optional[str] = None is_active: Optional[bool] = None remark: Optional[str] = None + custom_permissions: Optional[list[str]] = None # ---- 账号 ---- diff --git a/web/frontend/src/api/modules.ts b/web/frontend/src/api/modules.ts index ef72bce..5024547 100644 --- a/web/frontend/src/api/modules.ts +++ b/web/frontend/src/api/modules.ts @@ -15,6 +15,7 @@ export interface UserInfo { is_active: boolean; remark: string; permissions: string[]; + custom_permissions?: string[] | null; } export const authApi = { @@ -30,9 +31,10 @@ export const userApi = { list: () => api.get('/users'), create: (data: { username: string; password: string; role: string; remark?: string }) => api.post('/users', data), - update: (id: number, data: { password?: string; role?: string; is_active?: boolean; remark?: string }) => + update: (id: number, data: { password?: string; role?: string; is_active?: boolean; remark?: string; custom_permissions?: string[] | null }) => api.put(`/users/${id}`, data), delete: (id: number) => api.delete(`/users/${id}`), + listPermissions: () => api.get('/users/permissions/list'), }; export const accountApi = { diff --git a/web/frontend/src/layouts/MainLayout.tsx b/web/frontend/src/layouts/MainLayout.tsx index c9f23c1..5d4bc3c 100644 --- a/web/frontend/src/layouts/MainLayout.tsx +++ b/web/frontend/src/layouts/MainLayout.tsx @@ -46,7 +46,7 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) { } // 登录任务 - if (hasPerm(user, 'login:batch') || hasPerm(user, 'login:view_assigned')) { + if (hasPerm(user, 'login:batch') || hasPerm(user, 'login:view_all')) { menuItems.push({ key: '/login-tasks', label: '登录任务', icon: }); } diff --git a/web/frontend/src/pages/UsersPage.tsx b/web/frontend/src/pages/UsersPage.tsx index a0f2dea..6dff2a6 100644 --- a/web/frontend/src/pages/UsersPage.tsx +++ b/web/frontend/src/pages/UsersPage.tsx @@ -1,9 +1,11 @@ import { useEffect, useState } from 'react'; import { Table, Button, Modal, Form, Input, Select, Tag, Popconfirm, message, Space, + Checkbox, Divider, Alert, Tooltip, } from 'antd'; -import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; +import { PlusOutlined, EditOutlined, DeleteOutlined, SafetyOutlined } from '@ant-design/icons'; import { userApi, type UserInfo } from '../api/modules'; +import { getUser, hasPerm } from '../store/auth'; const ROLE_OPTIONS = [ { value: 'super_admin', label: '超级管理员' }, @@ -17,6 +19,21 @@ const ROLE_COLORS: Record = { support: 'green', }; +// 权限分组定义 +const PERMISSION_GROUPS = [ + { label: '用户管理', prefix: 'user:' }, + { label: '账号管理', prefix: 'account:' }, + { label: '登录任务', prefix: 'login:' }, + { label: 'Cookie', prefix: 'cookie:' }, + { label: '代理 & 白名单', prefix: ['proxy:', 'whitelist:'] }, + { label: '系统', prefix: ['system:', 'audit:'] }, +]; + +function isPrefixMatch(key: string, prefix: string | string[]): boolean { + if (Array.isArray(prefix)) return prefix.some(p => key.startsWith(p)); + return key.startsWith(prefix); +} + export default function UsersPage() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(false); @@ -24,6 +41,17 @@ export default function UsersPage() { const [editing, setEditing] = useState(null); const [form] = Form.useForm(); + // 权限管理 + const [permModalOpen, setPermModalOpen] = useState(false); + const [permUser, setPermUser] = useState(null); + const [permList, setPermList] = useState>({}); + const [rolePermMap, setRolePermMap] = useState>({}); + const [selectedPerms, setSelectedPerms] = useState([]); + const [useCustom, setUseCustom] = useState(false); + const [permLoading, setPermLoading] = useState(false); + + const canAssignPerm = hasPerm(getUser(), 'user:assign_permissions'); + const loadUsers = async () => { setLoading(true); try { @@ -86,6 +114,52 @@ export default function UsersPage() { } }; + // ---- 权限管理 ---- + const handlePermClick = async (user: UserInfo) => { + setPermUser(user); + setPermModalOpen(true); + setPermLoading(true); + try { + const data = await userApi.listPermissions(); + setPermList(data.permissions || {}); + setRolePermMap(data.role_permissions || {}); + // 初始化选中权限:如果用户有 custom_permissions 则使用,否则使用角色默认 + if (user.custom_permissions !== null && user.custom_permissions !== undefined) { + setUseCustom(true); + setSelectedPerms(user.custom_permissions); + } else { + setUseCustom(false); + setSelectedPerms(data.role_permissions?.[user.role] || []); + } + } catch (e: any) { + message.error(e.message); + } finally { + setPermLoading(false); + } + }; + + const handlePermSave = async () => { + if (!permUser) return; + setPermLoading(true); + try { + await userApi.update(permUser.id, { + custom_permissions: useCustom ? selectedPerms : null, + }); + message.success('权限已更新'); + setPermModalOpen(false); + loadUsers(); + } catch (e: any) { + message.error(e.message); + } finally { + setPermLoading(false); + } + }; + + const handleRoleChangeForPerm = (role: string) => { + const rolePerms = rolePermMap[role] || []; + setSelectedPerms(rolePerms); + }; + const columns = [ { title: 'ID', dataIndex: 'id', width: 60 }, { title: '用户名', dataIndex: 'username' }, @@ -97,18 +171,33 @@ export default function UsersPage() { return {label}; }, }, + { + title: '权限模式', + dataIndex: 'custom_permissions', + width: 100, + render: (_: any, record: UserInfo) => + record.custom_permissions !== null && record.custom_permissions !== undefined + ? 自定义 + : 角色默认, + }, { title: '状态', dataIndex: 'is_active', + width: 80, render: (active: boolean) => active ? 启用 : 禁用, }, - { title: '备注', dataIndex: 'remark' }, + { title: '备注', dataIndex: 'remark', ellipsis: true }, { title: '操作', - width: 160, + width: 200, render: (_: any, record: UserInfo) => ( + {canAssignPerm && ( + + + + )} {record.role !== 'super_admin' && ( handleDelete(record.id)}> @@ -133,6 +222,8 @@ export default function UsersPage() { size="small" pagination={{ pageSize: 20 }} /> + + {/* 编辑/新建用户 Modal */} + + {/* 权限管理 Modal */} + setPermModalOpen(false)} + onOk={handlePermSave} + okText="保存" + width={640} + confirmLoading={permLoading} + > + {permUser && ( + <> + +
+ 权限模式: +