I'll read the specification and planning documents to understand the project requirements and current progress.
✓ List directory spec (11 files)
✓ Read plan.md (615 lines)
Now let me read the spec files to understand the complete requirements:
✓ Read spec/ISO27001-資料庫設計(Database Schema Design).md (838 lines)
✓ Read spec/ISO27001資安文件管理系統詳細規劃.md (453 lines)
✓ Read spec/ISO27001-合規管理系統-主頁面 UI 設計.md (2855 lines)
✓ Read spec/ISO27001-MVP10規劃.md (773 lines)
✓ Read spec/ISO27001-MVP10-1實作.md (758 lines)
讓我開始檢查當前專案的狀態,確認已完成的進度,然後選擇下一個核心功能進行實作。
✓ Check Docker containers status
$ cd /root/projects/iso27001-system && docker-compose ps
↪ 2 lines...
✓ Check Docker containers status with modern syntax
$ cd /root/projects/iso27001-system && docker compose ps
↪ 6 lines...
很好!系統正在運行。讓我檢查目前的實作進度:
✓ Check frontend pages implementation
$ cd /root/projects/iso27001-system && ls -la frontend/src/pages/
↪ 8 lines...
根據 plan.md,我看到階段一的核心功能大部分已完成:
- ✅ 後端資產管理 API
- ✅ 前端 React 應用程式初始化
- ✅ 基礎架構與路由
- ✅ 資產管理頁面(列表、詳情、表單)
- ✅ 用戶認證系統(後端JWT)
- ✅ 前端登入系統(登入頁面、Token管理、路由保護)
**下一個待完成的核心小功能是**:**1.3.3 權限控制**
讓我開始實作**角色基礎權限控制(RBAC)**系統:
✓ List directory backend/accounts (16 files)
✓ Read backend/accounts/models.py (128 lines)
很好!已經有基本的角色定義。現在我需要:
1. 建立 Permission 裝飾器和中間件
2. 前端實作權限控制組件
3. API 端點加上權限驗證
讓我開始實作:
✓ Create backend/accounts/permissions.py (+353)
現在更新 User 的 serializer 以包含權限資訊:
✓ Read backend/accounts/serializers.py (154 lines)
✓ Edit backend/accounts/serializers.py (+18 -2)
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ 3 from django.contrib.auth.password_validation import validate_password │
│ 4 from django.core.exceptions import ValidationError │
│ 5 from .models import User, LoginHistory │
│ 6 + from .permissions import ROLE_PERMISSIONS, Permissions │
│ 7 from datetime import timedelta │
│ 8 from django.utils import timezone │
│ 9 │
│ 11 class UserSerializer(serializers.ModelSerializer): │
│ 12 """使用者序列化器""" │
│ 13 │
│ 14 + permissions = serializers.SerializerMethodField() │
│ 15 + role_display = serializers.CharField(source='get_role_display', read_only=True) │
│ 16 + │
│ 17 class Meta: │
│ 18 model = User │
│ 19 fields = [ │
│ 20 'id', 'username', 'email', 'first_name', 'last_name', │
│ 21 'employee_id', 'phone', 'department', 'job_title', 'avatar_url', │
│ 18 - 'role', 'is_active', 'is_staff', 'mfa_enabled', │
│ 22 + 'role', 'role_display', 'permissions', 'is_active', 'is_staff', │
│ 'mfa_enabled', │
│ 23 'last_login', 'created_at', 'updated_at' │
│ 24 ] │
│ 25 read_only_fields = ['id', 'last_login', 'created_at', 'updated_at'] │
│ 22 'role', 'is_active', 'is_staff', 'mfa_enabled', │
│ 23 'last_login', 'created_at', 'updated_at' │
│ 24 ] │
│ 21 - read_only_fields = ['id', 'last_login', 'created_at', 'updated_at'] │
│ 25 + read_only_fields = ['id', 'last_login', 'created_at', 'updated_at', │
│ 'permissions', 'role_display' │
│ 26 + │
│ 27 + def get_permissions(self, obj): │
│ 28 + """取得使用者的所有權限列表""" │
│ 29 + if obj.is_superuser or obj.role == 'super_admin': │
│ 30 + # 返回所有權限 │
│ 31 + return [attr for attr in dir(Permissions) if not attr.startswith('_')] │
│ 32 + │
│ 33 + role_perms = ROLE_PERMISSIONS.get(obj.role, []) │
│ 34 + if role_perms == 'all': │
│ 35 + return [attr for attr in dir(Permissions) if not attr.startswith('_')] │
│ 36 + │
│ 37 + return role_perms │
│ 38 │
│ 39 │
│ 40 class UserCreateSerializer(serializers.ModelSerializer): │
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
現在更新資產 ViewSet 以使用權限控制:
✓ Read backend/assets/views.py:1-50 (55 lines)
✓ Edit backend/assets/views.py (+9 -2)
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ 1 from rest_framework import viewsets, status, filters │
│ 2 from rest_framework.decorators import action │
│ 3 from rest_framework.response import Response │
│ 4 - from rest_framework.permissions import AllowAny │
│ 4 + from rest_framework.permissions import IsAuthenticated │
│ 5 from django_filters.rest_framework import DjangoFilterBackend │
│ 6 from django.db.models import Count, Q │
│ 7 + from accounts.permissions import HasPermission, IsOwnerOrAdmin, Permissions │
│ 8 from .models import Asset, AssetRelationship │
│ 9 from .serializers import ( │
│ 10 AssetSerializer, AssetListSerializer, AssetCreateSerializer, │
│ ──────────────────────────────────────────────────────────────────────────────────────────── │
│ 17 資產管理 ViewSet │
│ 18 │
│ 19 提供資產的 CRUD 操作以及統計、搜尋、篩選功能 │
│ 20 + │
│ 21 + 權限要求: │
│ 22 + - list/retrieve: asset_view │
│ 23 + - create: asset_add │
│ 24 + - update/partial_update: asset_change │
│ 25 + - destroy: asset_delete │
│ 26 """ │
│ 27 queryset = Asset.objects.filter(deleted_at__isnull=True) │
│ 21 - permission_classes = [AllowAny] # 暫時允許所有訪問,之後改為 IsAuthenticated │
│ ──────────────────────────────────────────────────────────────────────────────────────────── │
│ 28 + permission_classes = [IsAuthenticated, HasPermission] │
│ 29 filter_backends = [DjangoFilterBackend, filters.SearchFilter, │
│ filters.OrderingFilter] │
│ 30 │
│ 31 # 篩選欄位 │
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
✓ Read backend/assets/views.py:29-80 (57 lines)
✓ Edit backend/assets/views.py (+23)
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ 45 ordering_fields = ['asset_number', 'name', 'created_at', 'updated_at'] │
│ 46 ordering = ['-created_at'] │
│ 47 │
│ 48 + def get_required_permission(self): │
│ 49 + """根據動作返回所需權限""" │
│ 50 + action_permissions = { │
│ 51 + 'list': Permissions.ASSET_VIEW, │
│ 52 + 'retrieve': Permissions.ASSET_VIEW, │
│ 53 + 'create': Permissions.ASSET_ADD, │
│ 54 + 'update': Permissions.ASSET_CHANGE, │
│ 55 + 'partial_update': Permissions.ASSET_CHANGE, │
│ 56 + 'destroy': Permissions.ASSET_DELETE, │
│ 57 + 'statistics': Permissions.ASSET_VIEW, │
│ 58 + 'export': Permissions.ASSET_EXPORT, │
│ 59 + } │
│ 60 + return action_permissions.get(self.action, Permissions.ASSET_VIEW) │
│ 61 + │
│ 62 + def get_permissions(self): │
│ 63 + """動態設定權限""" │
│ 64 + self.required_permission = self.get_required_permission() │
│ 65 + return super().get_permissions() │
│ 66 + │
│ 67 def get_serializer_class(self): │
│ 68 """根據動作選擇序列化器""" │
│ 69 if self.action == 'list': │
│ 71 elif self.action == 'create': │
│ 72 return AssetCreateSerializer │
│ 73 return AssetSerializer │
│ 74 + │
│ 75 + def perform_create(self, serializer): │
│ 76 + """建立時自動設定建立者""" │
│ 77 + serializer.save(created_by=self.request.user) │
│ 78 │
│ 79 def perform_destroy(self, instance): │
│ 80 """軟刪除""" │
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
現在建立前端的權限控制組件和 Hook:
✗ Create frontend/src/hooks/usePermissions.ts
Parent directory does not exist
✓ Create hooks directory
$ mkdir -p /root/projects/iso27001-system/frontend/src/hooks
↪ 1 line...
✓ Create frontend/src/hooks/usePermissions.ts (+146)
現在創建權限控制組件:
✓ Create components directory
$ mkdir -p /root/projects/iso27001-system/frontend/src/components
↪ 1 line...
✓ Create frontend/src/components/PermissionGuard.tsx (+171)
現在更新 User 類型定義以包含權限:
✓ List directory frontend/src/types (7 files)
✓ Create frontend/src/types/user.ts (+44)
現在更新 AssetList 頁面,加入權限控制:
✓ Read frontend/src/pages/AssetList.tsx:1-60 (65 lines)
✓ Edit frontend/src/pages/AssetList.tsx (+4)
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ 12 Col, │
│ 13 Card, │
│ 14 Statistic, │
│ 15 + Tooltip, │
│ 16 } from 'antd'; │
│ 17 import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'; │
│ 18 import { │
│ 22 ExportOutlined, │
│ 23 ReloadOutlined, │
│ 24 EyeOutlined, │
│ 25 + LockOutlined, │
│ 26 } from '@ant-design/icons'; │
│ 27 import { useNavigate } from 'react-router-dom'; │
│ 28 import { assetsApi } from '../api/assets'; │
│ 29 import { Asset, AssetStatistics } from '../types/asset'; │
│ 30 + import { PermissionGuard, Can } from '../components/PermissionGuard'; │
│ 31 + import { Permissions } from '../hooks/usePermissions'; │
│ 32 │
│ 33 const { Search } = Input; │
│ 34 const { Option } = Select; │
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
✓ Read frontend/src/pages/AssetList.tsx:139-198 (65 lines)
✓ Read frontend/src/pages/AssetList.tsx:198-250 (58 lines)
✓ Edit frontend/src/pages/AssetList.tsx (+29 -15)
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ 232 > │
│ 233 查看 │
│ 234 │
│ 235 - } │
│ 238 - onClick={() => navigate(`/assets/${record.id}/edit`)} │
│ 239 - > │
│ 240 - 編輯 │
│ 241 - │
│ 242 - } │
│ 246 - onClick={() => handleDelete(record.id)} │
│ 247 - > │
│ 248 - 刪除 │
│ 249 - │
│ 235 +