feat: implement full RBAC role-based access control

Backend:
- deps.py: add require_admin, require_manager, require_labeler, require_guest_or_above
- user.py: all write endpoints require admin
- datasource.py: write/sync endpoints require admin
- metadata.py: sync endpoint requires admin
- classification.py: category/rule write requires admin; results query requires guest+ with data isolation
- project.py: GET requires manager with created_by filtering; DELETE checks ownership
- task.py: my-tasks requires labeler with assignee_id filtering; create-task requires manager
- dashboard.py: requires guest_or_above
- report.py: requires guest_or_above
- project_service: list_projects adds created_by filter; list_results adds project_ids filter

Frontend:
- stores/user.ts: add hasRole, hasAnyRole, isAdmin, isManager, isLabeler, isSuperadmin
- router/index.ts: add roles to route meta; beforeEach checks role permissions
- Layout.vue: filter menu routes by user roles
- System.vue: hide add/edit/delete buttons for non-admins
- DataSource.vue: hide add/edit/delete/sync buttons for non-admins
- Project.vue: hide add/delete buttons for non-admins
This commit is contained in:
hiderfong
2026-04-23 12:09:32 +08:00
parent 377e9cba22
commit 8b2bc84399
16 changed files with 245 additions and 59 deletions
+83 -1
View File
@@ -4,7 +4,7 @@ from jose import JWTError
from app.core.database import get_db from app.core.database import get_db
from app.core.security import decode_token from app.core.security import decode_token
from app.models.user import User from app.models.user import User, Role
from app.services import user_service from app.services import user_service
@@ -47,3 +47,85 @@ def get_current_active_user(
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
) -> User: ) -> User:
return current_user return current_user
# ===================== RBAC Dependencies =====================
# Role code constants
ROLE_SUPERADMIN = "superadmin"
ROLE_ADMIN = "admin"
ROLE_PROJECT_MANAGER = "project_manager"
ROLE_LABELER = "labeler"
ROLE_REVIEWER = "reviewer"
ROLE_GUEST = "guest"
# Role hierarchy (higher index = more permissions)
ROLE_LEVELS = {
ROLE_GUEST: 0,
ROLE_LABELER: 1,
ROLE_REVIEWER: 1,
ROLE_PROJECT_MANAGER: 2,
ROLE_ADMIN: 3,
ROLE_SUPERADMIN: 4,
}
def _get_user_role_codes(user: User) -> list[str]:
"""Get list of role codes for a user."""
return [r.code for r in user.roles]
def _has_role(user: User, role_code: str) -> bool:
"""Check if user has a specific role."""
return role_code in _get_user_role_codes(user)
def _has_any_role(user: User, role_codes: list[str]) -> bool:
"""Check if user has any of the specified roles."""
user_roles = _get_user_role_codes(user)
return any(r in user_roles for r in role_codes)
def _is_admin(user: User) -> bool:
"""Check if user is admin or superadmin."""
return user.is_superuser or _has_any_role(user, [ROLE_SUPERADMIN, ROLE_ADMIN])
def require_admin(current_user: User = Depends(get_current_user)) -> User:
"""Require admin or superadmin role."""
if not _is_admin(current_user):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="需要管理员权限",
)
return current_user
def require_manager(current_user: User = Depends(get_current_user)) -> User:
"""Require project manager or above."""
if _is_admin(current_user):
return current_user
if _has_role(current_user, ROLE_PROJECT_MANAGER):
return current_user
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="需要项目负责人及以上权限",
)
def require_labeler(current_user: User = Depends(get_current_user)) -> User:
"""Require labeler or above (excluding guest)."""
if _is_admin(current_user):
return current_user
allowed = [ROLE_PROJECT_MANAGER, ROLE_LABELER, ROLE_REVIEWER]
if _has_any_role(current_user, allowed):
return current_user
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="权限不足",
)
def require_guest_or_above(current_user: User = Depends(get_current_user)) -> User:
"""Any authenticated user (including guest)."""
return current_user
+41 -10
View File
@@ -11,7 +11,7 @@ from app.schemas.classification import (
) )
from app.schemas.common import ResponseModel, ListResponse from app.schemas.common import ResponseModel, ListResponse
from app.services import classification_service, classification_engine from app.services import classification_service, classification_engine
from app.api.deps import get_current_user from app.api.deps import get_current_user, require_admin, require_labeler, require_guest_or_above, _is_admin, ROLE_PROJECT_MANAGER
router = APIRouter() router = APIRouter()
@@ -39,7 +39,7 @@ def list_categories(
def create_category( def create_category(
req: CategoryCreate, req: CategoryCreate,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_admin),
): ):
item = classification_service.create_category(db, req) item = classification_service.create_category(db, req)
return ResponseModel(data=CategoryOut.model_validate(item)) return ResponseModel(data=CategoryOut.model_validate(item))
@@ -50,7 +50,7 @@ def update_category(
category_id: int, category_id: int,
req: CategoryUpdate, req: CategoryUpdate,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_admin),
): ):
db_obj = classification_service.get_category(db, category_id) db_obj = classification_service.get_category(db, category_id)
if not db_obj: if not db_obj:
@@ -64,7 +64,7 @@ def update_category(
def delete_category( def delete_category(
category_id: int, category_id: int,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_admin),
): ):
classification_service.delete_category(db, category_id) classification_service.delete_category(db, category_id)
return ResponseModel(message="删除成功") return ResponseModel(message="删除成功")
@@ -103,7 +103,7 @@ def list_rules(
def create_rule( def create_rule(
req: RecognitionRuleCreate, req: RecognitionRuleCreate,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_admin),
): ):
item = classification_service.create_rule(db, req) item = classification_service.create_rule(db, req)
data = RecognitionRuleOut.model_validate(item) data = RecognitionRuleOut.model_validate(item)
@@ -118,7 +118,7 @@ def update_rule(
rule_id: int, rule_id: int,
req: RecognitionRuleUpdate, req: RecognitionRuleUpdate,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_admin),
): ):
db_obj = classification_service.get_rule(db, rule_id) db_obj = classification_service.get_rule(db, rule_id)
if not db_obj: if not db_obj:
@@ -136,7 +136,7 @@ def update_rule(
def delete_rule( def delete_rule(
rule_id: int, rule_id: int,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_admin),
): ):
classification_service.delete_rule(db, rule_id) classification_service.delete_rule(db, rule_id)
return ResponseModel(message="删除成功") return ResponseModel(message="删除成功")
@@ -159,10 +159,42 @@ def list_results(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=500), page_size: int = Query(20, ge=1, le=500),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_guest_or_above),
): ):
from app.services.project_service import list_results as _list_results from app.services.project_service import list_results as _list_results
items, total = _list_results(db, project_id=project_id, keyword=keyword, page=page, page_size=page_size) from app.models.project import ClassificationProject, ClassificationTask
from app.api.deps import _has_role
# Data isolation: compute allowed project IDs for non-admin users
allowed_project_ids = None
if not _is_admin(current_user):
if _has_role(current_user, ROLE_PROJECT_MANAGER):
# Project managers see their own projects
allowed_project_ids = [
p.id for p in db.query(ClassificationProject.id).filter(
ClassificationProject.created_by == current_user.id
).all()
]
elif _has_role(current_user, 'labeler') or _has_role(current_user, 'reviewer'):
# Labelers/reviewers see projects where they have tasks
task_projects = db.query(ClassificationTask.project_id).filter(
ClassificationTask.assignee_id == current_user.id
).distinct().all()
allowed_project_ids = [p[0] for p in task_projects]
else:
# Guests see all results (read-only)
allowed_project_ids = None
if allowed_project_ids is not None and not allowed_project_ids:
allowed_project_ids = []
# If a specific project_id is requested, check permission
if project_id and allowed_project_ids is not None and project_id not in allowed_project_ids:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权查看此项目")
items, total = _list_results(
db, project_id=project_id, keyword=keyword, page=page, page_size=page_size,
project_ids=allowed_project_ids,
)
data = [] data = []
for r in items: for r in items:
@@ -171,7 +203,6 @@ def list_results(
database = table.database if table else None database = table.database if table else None
source = database.source if database else None source = database.source if database else None
# Filter by level_id if specified
if level_id and r.level_id != level_id: if level_id and r.level_id != level_id:
continue continue
+3 -3
View File
@@ -9,7 +9,7 @@ from app.models.metadata import DataSource, DataTable, DataColumn
from app.models.project import ClassificationResult, ClassificationProject from app.models.project import ClassificationResult, ClassificationProject
from app.models.classification import Category, DataLevel from app.models.classification import Category, DataLevel
from app.schemas.common import ResponseModel from app.schemas.common import ResponseModel
from app.api.deps import get_current_user from app.api.deps import get_current_user, require_guest_or_above, _is_admin
router = APIRouter() router = APIRouter()
@@ -17,7 +17,7 @@ router = APIRouter()
@router.get("/stats") @router.get("/stats")
def get_dashboard_stats( def get_dashboard_stats(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_guest_or_above),
): ):
"""Dashboard overview statistics based on real DB data.""" """Dashboard overview statistics based on real DB data."""
data_sources = db.query(DataSource).count() data_sources = db.query(DataSource).count()
@@ -42,7 +42,7 @@ def get_dashboard_stats(
@router.get("/distribution") @router.get("/distribution")
def get_dashboard_distribution( def get_dashboard_distribution(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_guest_or_above),
): ):
"""Distribution data for charts based on real DB data.""" """Distribution data for charts based on real DB data."""
# Level distribution # Level distribution
+4 -4
View File
@@ -7,7 +7,7 @@ from app.models.user import User
from app.schemas.datasource import DataSourceCreate, DataSourceUpdate, DataSourceOut, DataSourceTest from app.schemas.datasource import DataSourceCreate, DataSourceUpdate, DataSourceOut, DataSourceTest
from app.schemas.common import ResponseModel, ListResponse from app.schemas.common import ResponseModel, ListResponse
from app.services import datasource_service from app.services import datasource_service
from app.api.deps import get_current_user from app.api.deps import get_current_user, require_admin
router = APIRouter() router = APIRouter()
@@ -41,7 +41,7 @@ def get_datasource(
def create_datasource( def create_datasource(
req: DataSourceCreate, req: DataSourceCreate,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_admin),
): ):
item = datasource_service.create_datasource(db, req, current_user.id) item = datasource_service.create_datasource(db, req, current_user.id)
return ResponseModel(data=DataSourceOut.model_validate(item)) return ResponseModel(data=DataSourceOut.model_validate(item))
@@ -52,7 +52,7 @@ def update_datasource(
source_id: int, source_id: int,
req: DataSourceUpdate, req: DataSourceUpdate,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_admin),
): ):
db_obj = datasource_service.get_datasource(db, source_id) db_obj = datasource_service.get_datasource(db, source_id)
if not db_obj: if not db_obj:
@@ -66,7 +66,7 @@ def update_datasource(
def delete_datasource( def delete_datasource(
source_id: int, source_id: int,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_admin),
): ):
datasource_service.delete_datasource(db, source_id) datasource_service.delete_datasource(db, source_id)
return ResponseModel(message="删除成功") return ResponseModel(message="删除成功")
+2 -2
View File
@@ -7,7 +7,7 @@ from app.models.user import User
from app.schemas.metadata import DatabaseOut, DataTableOut, DataColumnOut from app.schemas.metadata import DatabaseOut, DataTableOut, DataColumnOut
from app.schemas.common import ResponseModel, ListResponse from app.schemas.common import ResponseModel, ListResponse
from app.services import metadata_service from app.services import metadata_service
from app.api.deps import get_current_user from app.api.deps import get_current_user, require_admin
router = APIRouter() router = APIRouter()
@@ -60,7 +60,7 @@ def list_columns(
def sync_metadata( def sync_metadata(
source_id: int, source_id: int,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_admin),
): ):
result = metadata_service.sync_metadata(db, source_id, current_user.id) result = metadata_service.sync_metadata(db, source_id, current_user.id)
return ResponseModel(data=result) return ResponseModel(data=result)
+14 -5
View File
@@ -6,7 +6,7 @@ from app.core.database import get_db
from app.models.user import User from app.models.user import User
from app.schemas.common import ResponseModel, ListResponse from app.schemas.common import ResponseModel, ListResponse
from app.services import project_service from app.services import project_service
from app.api.deps import get_current_user from app.api.deps import get_current_user, require_admin, require_manager, _is_admin
router = APIRouter() router = APIRouter()
@@ -17,9 +17,11 @@ def list_projects(
page_size: int = Query(20, ge=1, le=500), page_size: int = Query(20, ge=1, le=500),
keyword: Optional[str] = Query(None), keyword: Optional[str] = Query(None),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_manager),
): ):
items, total = project_service.list_projects(db, keyword=keyword, page=page, page_size=page_size) # Data isolation: non-admin users only see their own projects
created_by = None if _is_admin(current_user) else current_user.id
items, total = project_service.list_projects(db, keyword=keyword, page=page, page_size=page_size, created_by=created_by)
data = [] data = []
for p in items: for p in items:
stats = project_service.get_project_stats(db, p.id) stats = project_service.get_project_stats(db, p.id)
@@ -43,7 +45,7 @@ def create_project(
target_source_ids: Optional[str] = None, target_source_ids: Optional[str] = None,
description: Optional[str] = None, description: Optional[str] = None,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_manager),
): ):
item = project_service.create_project( item = project_service.create_project(
db, name=name, template_id=template_id, db, name=name, template_id=template_id,
@@ -85,6 +87,13 @@ def delete_project(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
p = project_service.get_project(db, project_id)
if not p:
from fastapi import HTTPException, status
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
# Only admin or project creator can delete
if not _is_admin(current_user) and p.created_by != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权删除此项目")
project_service.delete_project(db, project_id) project_service.delete_project(db, project_id)
return ResponseModel(message="删除成功") return ResponseModel(message="删除成功")
@@ -93,7 +102,7 @@ def delete_project(
def project_auto_classify( def project_auto_classify(
project_id: int, project_id: int,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_manager),
): ):
from app.services.classification_engine import run_auto_classification from app.services.classification_engine import run_auto_classification
result = run_auto_classification(db, project_id) result = run_auto_classification(db, project_id)
+2 -2
View File
@@ -7,7 +7,7 @@ from app.models.user import User
from app.models.project import ClassificationResult, ClassificationProject from app.models.project import ClassificationResult, ClassificationProject
from app.models.classification import Category, DataLevel from app.models.classification import Category, DataLevel
from app.schemas.common import ResponseModel from app.schemas.common import ResponseModel
from app.api.deps import get_current_user from app.api.deps import get_current_user, require_guest_or_above
from app.services import report_service from app.services import report_service
router = APIRouter() router = APIRouter()
@@ -16,7 +16,7 @@ router = APIRouter()
@router.get("/stats") @router.get("/stats")
def get_report_stats( def get_report_stats(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_guest_or_above),
): ):
"""Global report statistics.""" """Global report statistics."""
total = db.query(ClassificationResult).count() total = db.query(ClassificationResult).count()
+6 -4
View File
@@ -5,7 +5,7 @@ from sqlalchemy.orm import Session
from app.core.database import get_db from app.core.database import get_db
from app.models.user import User from app.models.user import User
from app.schemas.common import ResponseModel, ListResponse from app.schemas.common import ResponseModel, ListResponse
from app.api.deps import get_current_user from app.api.deps import get_current_user, require_manager, require_labeler, _is_admin
from app.services import task_service, project_service from app.services import task_service, project_service
router = APIRouter() router = APIRouter()
@@ -15,9 +15,11 @@ router = APIRouter()
def my_tasks( def my_tasks(
status: Optional[str] = Query(None), status: Optional[str] = Query(None),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_labeler),
): ):
items, _ = task_service.list_tasks(db, assignee_id=current_user.id, status=status) # Data isolation: non-admin users only see tasks assigned to them
assignee_id = None if _is_admin(current_user) else current_user.id
items, _ = task_service.list_tasks(db, assignee_id=assignee_id, status=status)
data = [] data = []
for t in items: for t in items:
project = project_service.get_project(db, t.project_id) project = project_service.get_project(db, t.project_id)
@@ -100,7 +102,7 @@ def create_task_for_project(
assignee_id: int, assignee_id: int,
target_type: str = Query("column"), target_type: str = Query("column"),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_manager),
): ):
task = task_service.create_task( task = task_service.create_task(
db, project_id=project_id, name=name, db, project_id=project_id, name=name,
+5 -5
View File
@@ -8,7 +8,7 @@ from app.models.user import Role, Dept
from app.schemas.user import UserCreate, UserUpdate, UserOut, RoleOut, DeptOut from app.schemas.user import UserCreate, UserUpdate, UserOut, RoleOut, DeptOut
from app.schemas.common import ResponseModel, ListResponse, PageParams from app.schemas.common import ResponseModel, ListResponse, PageParams
from app.services import user_service from app.services import user_service
from app.api.deps import get_current_user from app.api.deps import get_current_user, require_admin
router = APIRouter() router = APIRouter()
@@ -24,7 +24,7 @@ def list_users(
page_size: int = Query(20, ge=1, le=500), page_size: int = Query(20, ge=1, le=500),
keyword: Optional[str] = Query(None), keyword: Optional[str] = Query(None),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_admin),
): ):
items, total = user_service.list_users(db, keyword=keyword, page=page, page_size=page_size) items, total = user_service.list_users(db, keyword=keyword, page=page, page_size=page_size)
return ListResponse(data=[UserOut.model_validate(u) for u in items], total=total, page=page, page_size=page_size) return ListResponse(data=[UserOut.model_validate(u) for u in items], total=total, page=page, page_size=page_size)
@@ -34,7 +34,7 @@ def list_users(
def create_user( def create_user(
req: UserCreate, req: UserCreate,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_admin),
): ):
user = user_service.create_user(db, req) user = user_service.create_user(db, req)
return ResponseModel(data=UserOut.model_validate(user)) return ResponseModel(data=UserOut.model_validate(user))
@@ -45,7 +45,7 @@ def update_user(
user_id: int, user_id: int,
req: UserUpdate, req: UserUpdate,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_admin),
): ):
user = user_service.get_user_by_id(db, user_id) user = user_service.get_user_by_id(db, user_id)
if not user: if not user:
@@ -59,7 +59,7 @@ def update_user(
def delete_user( def delete_user(
user_id: int, user_id: int,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(require_admin),
): ):
user_service.delete_user(db, user_id) user_service.delete_user(db, user_id)
return ResponseModel(message="删除成功") return ResponseModel(message="删除成功")
+6 -1
View File
@@ -12,11 +12,13 @@ def get_project(db: Session, project_id: int) -> Optional[ClassificationProject]
def list_projects( def list_projects(
db: Session, keyword: Optional[str] = None, page: int = 1, page_size: int = 20 db: Session, keyword: Optional[str] = None, page: int = 1, page_size: int = 20, created_by: Optional[int] = None
) -> Tuple[List[ClassificationProject], int]: ) -> Tuple[List[ClassificationProject], int]:
query = db.query(ClassificationProject) query = db.query(ClassificationProject)
if keyword: if keyword:
query = query.filter(ClassificationProject.name.contains(keyword)) query = query.filter(ClassificationProject.name.contains(keyword))
if created_by:
query = query.filter(ClassificationProject.created_by == created_by)
total = query.count() total = query.count()
items = query.order_by(ClassificationProject.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all() items = query.order_by(ClassificationProject.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
return items, total return items, total
@@ -82,10 +84,13 @@ def list_results(
keyword: Optional[str] = None, keyword: Optional[str] = None,
page: int = 1, page: int = 1,
page_size: int = 50, page_size: int = 50,
project_ids: Optional[List[int]] = None,
) -> Tuple[List[ClassificationResult], int]: ) -> Tuple[List[ClassificationResult], int]:
query = db.query(ClassificationResult) query = db.query(ClassificationResult)
if project_id: if project_id:
query = query.filter(ClassificationResult.project_id == project_id) query = query.filter(ClassificationResult.project_id == project_id)
if project_ids:
query = query.filter(ClassificationResult.project_id.in_(project_ids))
if table_id: if table_id:
query = query.join(DataColumn).filter(DataColumn.table_id == table_id) query = query.join(DataColumn).filter(DataColumn.table_id == table_id)
if status: if status:
+7 -1
View File
@@ -122,7 +122,13 @@ const pageTitle = computed(() => (route.meta?.title as string) || 'DataPointer')
const menuRoutes = computed(() => { const menuRoutes = computed(() => {
const layout = router.getRoutes().find((r) => r.name === 'Layout') const layout = router.getRoutes().find((r) => r.name === 'Layout')
return layout?.children.filter((r) => r.meta?.title) || [] const routes = layout?.children.filter((r) => r.meta?.title) || []
// Filter by role permissions
return routes.filter((r) => {
const allowedRoles = r.meta?.roles as string[] | undefined
if (!allowedRoles || allowedRoles.length === 0) return true
return userStore.hasAnyRole(allowedRoles)
})
}) })
function handleCommand(cmd: string) { function handleCommand(cmd: string) {
+31 -12
View File
@@ -18,55 +18,55 @@ const routes = [
path: 'dashboard', path: 'dashboard',
name: 'Dashboard', name: 'Dashboard',
component: () => import('@/views/dashboard/Dashboard.vue'), component: () => import('@/views/dashboard/Dashboard.vue'),
meta: { title: '首页', icon: 'HomeFilled' }, meta: { title: '首页', icon: 'HomeFilled', roles: ['superadmin', 'admin', 'project_manager', 'labeler', 'reviewer', 'guest'] },
}, },
{ {
path: 'datasource', path: 'datasource',
name: 'DataSource', name: 'DataSource',
component: () => import('@/views/datasource/DataSource.vue'), component: () => import('@/views/datasource/DataSource.vue'),
meta: { title: '数据源管理', icon: 'DataLine' }, meta: { title: '数据源管理', icon: 'DataLine', roles: ['superadmin', 'admin', 'project_manager', 'labeler', 'reviewer', 'guest'] },
}, },
{ {
path: 'metadata', path: 'metadata',
name: 'Metadata', name: 'Metadata',
component: () => import('@/views/metadata/Metadata.vue'), component: () => import('@/views/metadata/Metadata.vue'),
meta: { title: '数据资产', icon: 'FolderOpened' }, meta: { title: '数据资产', icon: 'FolderOpened', roles: ['superadmin', 'admin', 'project_manager', 'labeler', 'reviewer', 'guest'] },
}, },
{ {
path: 'category', path: 'category',
name: 'Category', name: 'Category',
component: () => import('@/views/category/Category.vue'), component: () => import('@/views/category/Category.vue'),
meta: { title: '分类分级标准', icon: 'Collection' }, meta: { title: '分类分级标准', icon: 'Collection', roles: ['superadmin', 'admin', 'project_manager', 'labeler', 'reviewer', 'guest'] },
}, },
{ {
path: 'project', path: 'project',
name: 'Project', name: 'Project',
component: () => import('@/views/project/Project.vue'), component: () => import('@/views/project/Project.vue'),
meta: { title: '项目管理', icon: 'List' }, meta: { title: '项目管理', icon: 'List', roles: ['superadmin', 'admin', 'project_manager'] },
}, },
{ {
path: 'task', path: 'task',
name: 'Task', name: 'Task',
component: () => import('@/views/task/Task.vue'), component: () => import('@/views/task/Task.vue'),
meta: { title: '我的任务', icon: 'EditPen' }, meta: { title: '我的任务', icon: 'EditPen', roles: ['superadmin', 'admin', 'project_manager', 'labeler', 'reviewer'] },
}, },
{ {
path: 'classification', path: 'classification',
name: 'Classification', name: 'Classification',
component: () => import('@/views/classification/Classification.vue'), component: () => import('@/views/classification/Classification.vue'),
meta: { title: '分类分级结果', icon: 'DocumentChecked' }, meta: { title: '分类分级结果', icon: 'DocumentChecked', roles: ['superadmin', 'admin', 'project_manager', 'labeler', 'reviewer', 'guest'] },
}, },
{ {
path: 'report', path: 'report',
name: 'Report', name: 'Report',
component: () => import('@/views/report/Report.vue'), component: () => import('@/views/report/Report.vue'),
meta: { title: '报表统计', icon: 'TrendCharts' }, meta: { title: '报表统计', icon: 'TrendCharts', roles: ['superadmin', 'admin', 'project_manager'] },
}, },
{ {
path: 'system', path: 'system',
name: 'System', name: 'System',
component: () => import('@/views/system/System.vue'), component: () => import('@/views/system/System.vue'),
meta: { title: '系统管理', icon: 'Setting' }, meta: { title: '系统管理', icon: 'Setting', roles: ['superadmin', 'admin'] },
}, },
], ],
}, },
@@ -83,11 +83,30 @@ const router = createRouter({
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const userStore = useUserStore() const userStore = useUserStore()
if (!to.meta.public && !userStore.token) {
next('/login') // Public routes (login)
} else { if (to.meta.public) {
next() next()
return
} }
// Not logged in
if (!userStore.token) {
next('/login')
return
}
// Check role permissions
const allowedRoles = to.meta.roles as string[] | undefined
if (allowedRoles && allowedRoles.length > 0) {
const hasPermission = userStore.hasAnyRole(allowedRoles)
if (!hasPermission) {
next('/dashboard')
return
}
}
next()
}) })
export default router export default router
+26
View File
@@ -8,6 +8,25 @@ export const useUserStore = defineStore('user', () => {
const userInfo = ref<UserInfo | null>(null) const userInfo = ref<UserInfo | null>(null)
const isLoggedIn = computed(() => !!token.value) const isLoggedIn = computed(() => !!token.value)
// Extract role codes from user info
const roleCodes = computed(() => {
if (!userInfo.value?.roles) return []
return userInfo.value.roles.map((r: any) => r.code)
})
const isSuperadmin = computed(() => roleCodes.value.includes('superadmin'))
const isAdmin = computed(() => isSuperadmin.value || roleCodes.value.includes('admin'))
const isManager = computed(() => isAdmin.value || roleCodes.value.includes('project_manager'))
const isLabeler = computed(() => isManager.value || roleCodes.value.includes('labeler') || roleCodes.value.includes('reviewer'))
function hasRole(code: string): boolean {
return roleCodes.value.includes(code)
}
function hasAnyRole(codes: string[]): boolean {
return codes.some((c) => roleCodes.value.includes(c))
}
async function login(username: string, password: string) { async function login(username: string, password: string) {
const res = await apiLogin(username, password) const res = await apiLogin(username, password)
token.value = res.access_token token.value = res.access_token
@@ -39,6 +58,13 @@ export const useUserStore = defineStore('user', () => {
token, token,
userInfo, userInfo,
isLoggedIn, isLoggedIn,
roleCodes,
isSuperadmin,
isAdmin,
isManager,
isLabeler,
hasRole,
hasAnyRole,
login, login,
fetchUserInfo, fetchUserInfo,
logout, logout,
+6 -4
View File
@@ -2,7 +2,7 @@
<div class="page-container"> <div class="page-container">
<div class="page-header"> <div class="page-header">
<h2 class="page-title">数据源管理</h2> <h2 class="page-title">数据源管理</h2>
<el-button type="primary" @click="handleAdd"> <el-button v-if="userStore.isAdmin" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增数据源 <el-icon><Plus /></el-icon>新增数据源
</el-button> </el-button>
</div> </div>
@@ -44,9 +44,9 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" width="260" fixed="right"> <el-table-column label="操作" width="260" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button> <el-button v-if="userStore.isAdmin" type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="primary" link size="small" @click="handleSync(row)">同步元数据</el-button> <el-button v-if="userStore.isAdmin" type="primary" link size="small" @click="handleSync(row)">同步元数据</el-button>
<el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button> <el-button v-if="userStore.isAdmin" type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -114,9 +114,11 @@
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search } from '@element-plus/icons-vue' import { Plus, Search } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/user'
import { getDataSources, createDataSource, updateDataSource, deleteDataSource, testConnection, syncMetadata } from '@/api/datasource' import { getDataSources, createDataSource, updateDataSource, deleteDataSource, testConnection, syncMetadata } from '@/api/datasource'
import type { DataSourceItem, DataSourceForm } from '@/api/datasource' import type { DataSourceItem, DataSourceForm } from '@/api/datasource'
const userStore = useUserStore()
const loading = ref(false) const loading = ref(false)
const tableData = ref<DataSourceItem[]>([]) const tableData = ref<DataSourceItem[]>([])
const page = ref(1) const page = ref(1)
+4 -2
View File
@@ -2,7 +2,7 @@
<div class="page-container"> <div class="page-container">
<div class="page-header"> <div class="page-header">
<h2 class="page-title">项目管理</h2> <h2 class="page-title">项目管理</h2>
<el-button type="primary" @click="handleAdd"> <el-button v-if="userStore.isAdmin" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新建项目 <el-icon><Plus /></el-icon>新建项目
</el-button> </el-button>
</div> </div>
@@ -52,7 +52,7 @@
</div> </div>
<div class="project-actions"> <div class="project-actions">
<el-button type="primary" size="small" @click="handleAutoClassify(p)">自动分类</el-button> <el-button type="primary" size="small" @click="handleAutoClassify(p)">自动分类</el-button>
<el-button type="danger" link size="small" @click="handleDelete(p)">删除</el-button> <el-button v-if="userStore.isAdmin" type="danger" link size="small" @click="handleDelete(p)">删除</el-button>
</div> </div>
</div> </div>
</el-col> </el-col>
@@ -91,6 +91,7 @@
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search } from '@element-plus/icons-vue' import { Plus, Search } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/user'
import { getProjects, createProject, deleteProject, autoClassifyProject } from '@/api/project' import { getProjects, createProject, deleteProject, autoClassifyProject } from '@/api/project'
import { getTemplates } from '@/api/classification' import { getTemplates } from '@/api/classification'
import { getDataSources } from '@/api/datasource' import { getDataSources } from '@/api/datasource'
@@ -103,6 +104,7 @@ const pageSize = ref(20)
const total = ref(0) const total = ref(0)
const keyword = ref('') const keyword = ref('')
const userStore = useUserStore()
const dialogVisible = ref(false) const dialogVisible = ref(false)
const submitLoading = ref(false) const submitLoading = ref(false)
const formRef = ref() const formRef = ref()
+5 -3
View File
@@ -10,7 +10,7 @@
<el-button type="primary" size="small" @click="handleSearch"> <el-button type="primary" size="small" @click="handleSearch">
<el-icon><Search /></el-icon>查询 <el-icon><Search /></el-icon>查询
</el-button> </el-button>
<el-button type="success" size="small" @click="openAdd"> <el-button v-if="userStore.isAdmin" type="success" size="small" @click="openAdd">
<el-icon><Plus /></el-icon>新增用户 <el-icon><Plus /></el-icon>新增用户
</el-button> </el-button>
</div> </div>
@@ -39,8 +39,8 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" width="160" fixed="right"> <el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" size="small" @click="openEdit(row)">编辑</el-button> <el-button v-if="userStore.isAdmin" link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" size="small" :disabled="row.is_superuser" @click="handleDelete(row)"> <el-button v-if="userStore.isAdmin" link type="danger" size="small" :disabled="row.is_superuser" @click="handleDelete(row)">
删除 删除
</el-button> </el-button>
</template> </template>
@@ -112,10 +112,12 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus } from '@element-plus/icons-vue' import { Search, Plus } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/user'
import { getUsers, createUser, updateUser, deleteUser } from '@/api/user' import { getUsers, createUser, updateUser, deleteUser } from '@/api/user'
import { getRoles, getDepts } from '@/api/user' import { getRoles, getDepts } from '@/api/user'
import type { UserItem, RoleItem, DeptItem } from '@/api/user' import type { UserItem, RoleItem, DeptItem } from '@/api/user'
const userStore = useUserStore()
const activeTab = ref('users') const activeTab = ref('users')
const userList = ref<UserItem[]>([]) const userList = ref<UserItem[]>([])
const userLoading = ref(false) const userLoading = ref(false)