From ee00d68b9f8a304bda2464fa9422c8db8aa01dd9 Mon Sep 17 00:00:00 2001 From: Azusa Date: Fri, 3 Oct 2025 20:12:33 +0800 Subject: [PATCH 1/4] feat: implement RBAC for project and board management, add team and project models, and update routes --- .gitignore | 1 + backend/database/migrate.go | 6 +- backend/database/seed.go | 25 +-- backend/handlers/board/board.go | 11 +- backend/middleware/rbac.go | 167 ++++++++++++++++++ backend/models/project.go | 13 +- backend/models/team.go | 42 +++++ backend/models/user.go | 12 +- backend/models/user_permission.go | 48 ----- backend/routes/routes.go | 81 +++++++-- backend/services/permission_service.go | 123 +++++++++++++ frontend/src/router/index.ts | 4 +- frontend/src/services/board-api.ts | 4 +- frontend/src/stores/board.ts | 4 +- .../src/views/dashboard/BoardListView.vue | 11 +- 15 files changed, 447 insertions(+), 105 deletions(-) create mode 100644 backend/middleware/rbac.go create mode 100644 backend/models/team.go delete mode 100644 backend/models/user_permission.go create mode 100644 backend/services/permission_service.go diff --git a/.gitignore b/.gitignore index 9ea26c9..5081f33 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ pnpm-debug.log* # Environment files .env +progress-wall.code-workspace diff --git a/backend/database/migrate.go b/backend/database/migrate.go index 4bceaab..a793872 100644 --- a/backend/database/migrate.go +++ b/backend/database/migrate.go @@ -15,10 +15,8 @@ func Migrate(db *gorm.DB) error { err := db.AutoMigrate( // 用户和权限相关 &models.User{}, - &models.UserPermission{}, - &models.UserPermissionGroup{}, - &models.UserPermissionGroupUser{}, - &models.UserPermissionGroup{}, + &models.Team{}, + &models.TeamMember{}, // 项目和看板相关 &models.Project{}, diff --git a/backend/database/seed.go b/backend/database/seed.go index 7764006..f4f534f 100644 --- a/backend/database/seed.go +++ b/backend/database/seed.go @@ -18,11 +18,6 @@ func Seed(db *gorm.DB) error { return err } - // 创建默认权限 - if err := createDefaultPermissions(db); err != nil { - return err - } - log.Println("基础数据初始化完成") return nil } @@ -43,6 +38,7 @@ func createAdminUser(db *gorm.DB) error { return nil } + log.Println("未找到admin用户,正在创建...") // 加密管理员密码 adminPasswordHash, err := utils.HashPassword("admin123") if err != nil { @@ -56,6 +52,7 @@ func createAdminUser(db *gorm.DB) error { Password: adminPasswordHash, Nickname: "系统管理员", Status: models.UserStatusEnabled, + SystemRole: models.SystemRoleAdmin, } if err := db.Create(&adminUser).Error; err != nil { @@ -65,21 +62,3 @@ func createAdminUser(db *gorm.DB) error { log.Println("默认用户创建成功: admin") return nil } - -// createDefaultPermissions 创建默认权限组 -// 该函数负责在系统初始化时创建默认的权限组,包括管理员权限组和访客权限组 -// 参数: db - 数据库连接实例 -// 返回: error - 如果创建失败则返回错误,否则返回nil -func createDefaultPermissions(db *gorm.DB) error { - adminPermissionGroup := models.UserPermissionGroup{ - GroupName: "admin", - PermissionStrings: "*", - } - db.Create(&adminPermissionGroup) - guestPermissionGroup := models.UserPermissionGroup{ - GroupName: "guest", - PermissionStrings: "users.list;users.detail", - } - db.Create(&guestPermissionGroup) - return nil -} diff --git a/backend/handlers/board/board.go b/backend/handlers/board/board.go index dcf96f6..c61e076 100644 --- a/backend/handlers/board/board.go +++ b/backend/handlers/board/board.go @@ -69,11 +69,18 @@ func (h *BoardHandler) CreateBoard(c *gin.Context) { return } + projectIDStr := c.Param("projectId") + projectID, err := strconv.ParseUint(projectIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Project ID"}) + return + } + var createBoardRequest struct { Name string `json:"name" binding:"required"` Description string `json:"description"` Color string `json:"color"` - ProjectID uint `json:"project_id" binding:"required"` + // ProjectID uint `json:"project_id" binding:"required"` } if err := c.ShouldBindJSON(&createBoardRequest); err != nil { @@ -85,7 +92,7 @@ func (h *BoardHandler) CreateBoard(c *gin.Context) { Name: createBoardRequest.Name, Description: createBoardRequest.Description, Color: createBoardRequest.Color, - ProjectID: createBoardRequest.ProjectID, + ProjectID: uint(projectID), OwnerID: userID, Status: models.BoardStatusActive, } diff --git a/backend/middleware/rbac.go b/backend/middleware/rbac.go new file mode 100644 index 0000000..01b5852 --- /dev/null +++ b/backend/middleware/rbac.go @@ -0,0 +1,167 @@ +package middleware + +import ( + "net/http" + "progress-wall-backend/models" + "progress-wall-backend/services" + "strconv" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// Role-based access control +type RBACMiddleware struct { + permService *services.PermissionService + db *gorm.DB +} + +func NewRBACMiddleware(permService *services.PermissionService, db *gorm.DB) *RBACMiddleware { + return &RBACMiddleware{ + permService: permService, + db: db, + } +} + +// Ensures the user is a System Administrator. +func (m *RBACMiddleware) RequireSysAdmin() gin.HandlerFunc { + return func(c *gin.Context) { + userID := c.GetUint("user_id") + + isAdmin, err := m.permService.IsSysAdmin(userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify system admin privileges"}) + c.Abort() + return + } + + if !isAdmin { + c.JSON(http.StatusForbidden, gin.H{"error": "System Admin privileges required"}) + c.Abort() + return + } + + c.Next() + } +} + +// Checks if the user has access to the project. +// level: "view" (access) or "manage" (admin). +// paramKey: the name of the URL parameter containing the ID (e.g., "projectId" or "boardId"). +// idType: "project" (direct project ID) or "board" (board ID, needs resolution to project). +func (m *RBACMiddleware) RequireProjectAccess(level string, paramKey string, idType string) gin.HandlerFunc { + return func(c *gin.Context) { + userID := c.GetUint("user_id") + idStr := c.Param(paramKey) + + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + c.Abort() + return + } + resourceID := uint(id) + + // Resolve ProjectID + var projectID uint + + switch idType { + case "project": + projectID = resourceID + + case "board": + var board models.Board + if err := m.db.Select("project_id").First(&board, resourceID).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Board not found"}) + c.Abort() + return + } + projectID = board.ProjectID + + case "column": + var column models.Column + if err := m.db.Preload("Board").First(&column, resourceID).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Column not found"}) + c.Abort() + return + } + projectID = column.Board.ProjectID + + case "task": + var task models.Task + if err := m.db.Select("project_id").First(&task, resourceID).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"}) + c.Abort() + return + } + + default: + c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid ID type for RBAC"}) + c.Abort() + return + } + + // Permission check + var allowed bool + var checkErr error + + if level == "manage" { + allowed, checkErr = m.permService.CanManageProject(userID, projectID) + } else { + allowed, checkErr = m.permService.CanAccessProject(userID, projectID) + } + + if checkErr != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Permission check failed"}) + c.Abort() + return + } + + if !allowed { + c.JSON(http.StatusForbidden, gin.H{"error": "Access denied: Insufficient permissions"}) + c.Abort() + return + } + + c.Next() + } +} + +// Checks if the user has access to the team. +// level: "view" or "manage". +func (m *RBACMiddleware) RequireTeamAccess(level string, paramKey string) gin.HandlerFunc { + return func(c *gin.Context) { + userID := c.GetUint("user_id") + teamIDStr := c.Param(paramKey) + + teamID, err := strconv.ParseUint(teamIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Team ID"}) + c.Abort() + return + } + + var allowed bool + var checkErr error + + if level == "manage" { + allowed, checkErr = m.permService.CanManageTeam(userID, uint(teamID)) + } else { + allowed, checkErr = m.permService.CanAccessTeam(userID, uint(teamID)) + } + + if checkErr != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Permission check failed"}) + c.Abort() + return + } + + if !allowed { + c.JSON(http.StatusForbidden, gin.H{"error": "Access denied: Insufficient team permissions"}) + c.Abort() + return + } + + c.Next() + } +} \ No newline at end of file diff --git a/backend/models/project.go b/backend/models/project.go index f466c84..8af15bf 100644 --- a/backend/models/project.go +++ b/backend/models/project.go @@ -16,12 +16,14 @@ type Project struct { StartDate *time.Time `json:"start_date" comment:"项目开始时间,可为空"` EndDate *time.Time `json:"end_date" comment:"项目结束时间,可为空"` OwnerID uint `json:"owner_id" gorm:"not null;index" comment:"项目所有者用户ID,必填,建立索引"` + TeamID uint `json:"team_id" gorm:"not null;index"` // The team this project belongs to CreatedAt time.Time `json:"created_at" comment:"创建时间"` UpdatedAt time.Time `json:"updated_at" comment:"更新时间"` DeletedAt gorm.DeletedAt `json:"-" gorm:"index" comment:"软删除时间,用于软删除标记"` // 关联关系 - Owner User `json:"owner,omitempty" gorm:"foreignKey:OwnerID" comment:"项目所有者,必须有项目根权限*.project.project_id.*"` + Owner User `json:"owner,omitempty" gorm:"foreignKey:OwnerID" comment:"项目所有者,必须有项目根权限"` + Team *Team `json:"team,omitempty" gorm:"foreignKey:TeamID"` Boards []Board `json:"boards,omitempty" gorm:"foreignKey:ProjectID"` Members []User `json:"members,omitempty" gorm:"many2many:project_members"` } @@ -32,7 +34,7 @@ type ProjectMember struct { ID uint `json:"id" gorm:"primaryKey;autoIncrement" comment:"关联记录唯一标识符,自增主键"` ProjectID uint `json:"project_id" gorm:"not null;index" comment:"项目ID,必填,建立索引"` UserID uint `json:"user_id" gorm:"not null;index" comment:"用户ID,必填,建立索引"` - ProjectPermission string `json:"project_permission" gorm:"type:text;comment:'项目权限,如*.project.project_id.*'" comment:"项目权限字符串,如*.project.project_id.*"` + Role ProjectRole `json:"role" gorm:"type:tinyint;default:1;comment:'Project role: 1=member, 2=admin'" comment:"项目角色,1=成员,2=管理员"` JoinedAt time.Time `json:"joined_at" comment:"加入项目时间"` CreatedAt time.Time `json:"created_at" comment:"创建时间"` UpdatedAt time.Time `json:"updated_at" comment:"更新时间"` @@ -60,3 +62,10 @@ const ( ProjectVisibilityPublic ProjectVisibility = 1 // 公开 ProjectVisibilityPrivate ProjectVisibility = 2 // 私有 ) + +type ProjectRole int + +const ( + ProjectRoleMember ProjectRole = 1 + ProjectRoleAdmin ProjectRole = 2 +) diff --git a/backend/models/team.go b/backend/models/team.go new file mode 100644 index 0000000..4960568 --- /dev/null +++ b/backend/models/team.go @@ -0,0 +1,42 @@ +package models + +import ( + "time" + "gorm.io/gorm" +) + +type TeamRole int + +const ( + TeamRoleMember TeamRole = 1 + TeamRoleAdmin TeamRole = 2 +) + +type Team struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + Name string `json:"name" gorm:"size:100;not null;comment:'Team name'"` + Description string `json:"description" gorm:"type:text"` + CreatorID uint `json:"creator_id" gorm:"not null;index"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + // associations + Creator *User `json:"creator,omitempty" gorm:"foreignKey:CreatorID"` + Projects []Project `json:"projects,omitempty" gorm:"foreignKey:TeamID"` // A team can have multiple projects + Members []User `json:"members,omitempty" gorm:"many2many:team_members;"` +} + +type TeamMember struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + TeamID uint `json:"team_id" gorm:"not null;index;uniqueIndex:uk_team_user"` + UserID uint `json:"user_id" gorm:"not null;index;uniqueIndex:uk_team_user"` + Role TeamRole `json:"role" gorm:"type:tinyint;default:1;comment:'Team role: 1=member, 2=admin'"` + JoinedAt time.Time `json:"joined_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + Team Team `json:"team" gorm:"foreignKey:TeamID"` + User User `json:"user" gorm:"foreignKey:UserID"` +} diff --git a/backend/models/user.go b/backend/models/user.go index 2e82d1e..1278294 100644 --- a/backend/models/user.go +++ b/backend/models/user.go @@ -16,15 +16,14 @@ type User struct { Nickname string `json:"nickname" gorm:"size:50" comment:"用户昵称,用于显示,最大50字符"` Avatar string `json:"avatar" gorm:"size:255" comment:"头像URL地址,最大255字符"` Phone string `json:"phone" gorm:"size:20" comment:"手机号码,最大20字符"` - Status UserStatus `json:"status" gorm:"type:tinyint;default:1;comment:'用户状态:1=正常,2=禁用,3=删除'" comment:"用户状态,1=正常,2=禁用,3=删除"` + Status UserStatus `json:"status" gorm:"type:tinyint;default:1;comment:'用户状态:1=正常,2=禁用,3=删除'"` + SystemRole SystemRole `json:"system_role" gorm:"type:tinyint;default:1;comment:'系统角色: 1=普通用户, 2=系统管理员'"` LastLogin *time.Time `json:"last_login" comment:"最后登录时间,可为空"` CreatedAt time.Time `json:"created_at" comment:"创建时间"` UpdatedAt time.Time `json:"updated_at" comment:"更新时间"` DeletedAt gorm.DeletedAt `json:"-" gorm:"index" comment:"软删除时间,用于软删除标记"` // 关联关系 - Permissions []UserPermission `json:"permissions,omitempty" gorm:"foreignKey:UserID"` - PermissionGroups []UserPermissionGroup `json:"permission_groups,omitempty" gorm:"many2many:user_permission_group_users"` Projects []Project `json:"projects,omitempty" gorm:"many2many:project_members"` CreatedTasks []Task `json:"created_tasks,omitempty" gorm:"foreignKey:CreatorID"` AssignedTasks []Task `json:"assigned_tasks,omitempty" gorm:"foreignKey:AssigneeID"` @@ -39,3 +38,10 @@ const ( UserStatusDisabled UserStatus = 2 // 禁用 UserStatusDeleted UserStatus = 3 // 删除 ) + +type SystemRole int + +const ( + SystemRoleUser SystemRole = 1 + SystemRoleAdmin SystemRole = 2 +) diff --git a/backend/models/user_permission.go b/backend/models/user_permission.go deleted file mode 100644 index 4cd62da..0000000 --- a/backend/models/user_permission.go +++ /dev/null @@ -1,48 +0,0 @@ -package models - -import ( - "time" - - "gorm.io/gorm" -) - -// UserPermission 用户权限表 -type UserPermission struct { - ID uint `json:"id" gorm:"primaryKey;autoIncrement"` - UserID uint `json:"user_id" gorm:"not null;index"` - PermissionString string `json:"permission_string" gorm:"not null;comment:'权限字符串,;分隔,如*.user.read;project.myproject.task.create'"` - GrantedBy uint `json:"granted_by" gorm:"not null;comment:'授权人ID'"` - ExpiresAt *time.Time `json:"expires_at" gorm:"comment:'权限过期时间,为空表示永不过期'"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` - - // 关联关系 - User User `json:"user,omitempty" gorm:"foreignKey:UserID"` - GrantedByUser User `json:"granted_by_user,omitempty" gorm:"foreignKey:GrantedBy"` -} - -// UserPermissionGroup 用户权限组表 -type UserPermissionGroup struct { - ID uint `json:"id" gorm:"primaryKey;autoIncrement"` - GroupName string `json:"group_name" gorm:"size:100;not null;comment:'权限组名称'"` - PermissionStrings string `json:"permission_strings" gorm:"type:text;not null;comment:'权限字符串列表,;分隔'"` - CreatedBy uint `json:"created_by" gorm:"not null;comment:'创建人ID'"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` - - // 关联关系 - Users []User `json:"users,omitempty" gorm:"many2many:user_permission_group_users"` - CreatedByUser User `json:"created_by_user,omitempty" gorm:"foreignKey:CreatedBy"` -} - -// UserPermissionGroupUser 用户权限组用户关联表 -type UserPermissionGroupUser struct { - ID uint `json:"id" gorm:"primaryKey;autoIncrement"` - GroupID uint `json:"group_id" gorm:"not null;index"` - UserID uint `json:"user_id" gorm:"not null;index"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` -} diff --git a/backend/routes/routes.go b/backend/routes/routes.go index f245a23..24e830e 100644 --- a/backend/routes/routes.go +++ b/backend/routes/routes.go @@ -11,6 +11,7 @@ import ( "progress-wall-backend/handlers/task" "progress-wall-backend/handlers/user" "progress-wall-backend/middleware" + "progress-wall-backend/services" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" @@ -34,6 +35,9 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine { corsConfig.AllowCredentials = true r.Use(cors.New(corsConfig)) + permService := services.NewPermissionService(db) + rbac := middleware.NewRBACMiddleware(permService, db) + // 初始化处理器 loginHandler := auth.NewLoginHandler(db, cfg) registerHandler := auth.NewRegisterHandler(db, cfg) @@ -63,25 +67,72 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine { // 看板相关 protected.GET("/boards", boardHandler.GetBoards) - protected.POST("/boards", boardHandler.CreateBoard) - protected.GET("/boards/:boardId", boardHandler.GetBoard) - protected.PUT("/boards/:boardId", boardHandler.UpdateBoard) - protected.DELETE("/boards/:boardId", boardHandler.DeleteBoard) + protected.POST("/projects/:projectId/boards", boardHandler.CreateBoard, + rbac.RequireProjectAccess("manage", "projectId", "project"), + boardHandler.CreateBoard, + ) + protected.GET("/boards/:boardId", + rbac.RequireProjectAccess("view", "boardId", "board"), + boardHandler.GetBoard, + ) + protected.PUT("/boards/:boardId", + rbac.RequireProjectAccess("manage", "boardId", "board"), + boardHandler.UpdateBoard, + ) + protected.DELETE("/boards/:boardId", + rbac.RequireProjectAccess("manage", "boardId", "board"), + boardHandler.DeleteBoard, + ) // 列相关 - protected.GET("/boards/:boardId/columns", columnHandler.GetColumns) - protected.POST("/boards/:boardId/columns", columnHandler.CreateColumn) - protected.GET("/columns/:columnId", columnHandler.GetColumn) - protected.PUT("/columns/:columnId", columnHandler.UpdateColumn) - protected.DELETE("/columns/:columnId", columnHandler.DeleteColumn) + protected.GET("/boards/:boardId/columns", + rbac.RequireProjectAccess("view", "boardId", "board"), + columnHandler.GetColumns, + ) + protected.POST("/boards/:boardId/columns", + // Only admins can create columns + rbac.RequireProjectAccess("manage", "boardId", "board"), + columnHandler.CreateColumn, + ) + protected.GET("/columns/:columnId", + rbac.RequireProjectAccess("view", "columnId", "column"), + columnHandler.GetColumn, + ) + protected.PUT("/columns/:columnId", + rbac.RequireProjectAccess("manage", "columnId", "column"), + columnHandler.UpdateColumn, + ) + protected.DELETE("/columns/:columnId", + rbac.RequireProjectAccess("manage", "columnId", "column"), + columnHandler.DeleteColumn, + ) // 任务相关 - protected.GET("/columns/:columnId/tasks", taskHandler.GetTasks) - protected.POST("/columns/:columnId/tasks", taskHandler.CreateTask) - protected.GET("/tasks/:taskId", taskHandler.GetTask) - protected.PUT("/tasks/:taskId", taskHandler.UpdateTask) - protected.DELETE("/tasks/:taskId", taskHandler.DeleteTask) - protected.PATCH("/tasks/:taskId/move", taskHandler.MoveTask) + protected.GET("/columns/:columnId/tasks", + rbac.RequireProjectAccess("view", "columnId", "column"), + taskHandler.GetTasks, + ) + protected.POST("/columns/:columnId/tasks", + rbac.RequireProjectAccess("view", "columnId", "column"), + taskHandler.CreateTask, + ) + protected.GET("/tasks/:taskId", + rbac.RequireProjectAccess("view", "taskId", "task"), + taskHandler.GetTask, + ) + protected.PUT("/tasks/:taskId", + rbac.RequireProjectAccess("view", "taskId", "task"), + taskHandler.UpdateTask, + ) + protected.DELETE("/tasks/:taskId", + rbac.RequireProjectAccess("view", "taskId", "task"), + taskHandler.DeleteTask, + ) + protected.PATCH("/tasks/:taskId/move", + rbac.RequireProjectAccess("view", "taskId", "task"), + taskHandler.MoveTask, + ) + // 看板活动日志 protected.GET("/boards/:board_id/activities", boardActivitiesHandler.GetBoardActivities) diff --git a/backend/services/permission_service.go b/backend/services/permission_service.go new file mode 100644 index 0000000..5499927 --- /dev/null +++ b/backend/services/permission_service.go @@ -0,0 +1,123 @@ +package services + +import ( + "errors" + "progress-wall-backend/models" + + "gorm.io/gorm" +) + +// PermissionService handles permission validation logic. +type PermissionService struct { + db *gorm.DB +} + +// NewPermissionService creates a new instance of PermissionService. +func NewPermissionService(db *gorm.DB) *PermissionService { + return &PermissionService{db: db} +} + +// Checks if the user is a system administrator +func (s *PermissionService) IsSysAdmin(userID uint) (bool, error) { + var user models.User + if err := s.db.Select("system_role").First(&user, userID).Error; err != nil { + return false, err + } + return user.SystemRole == models.SystemRoleAdmin, nil +} + +// Checks if the user is an admin of a specific team (Internal Use) +func (s *PermissionService) isTeamAdmin(userID, teamID uint) (bool, error) { + var member models.TeamMember + err := s.db.Where("user_id = ? AND team_id = ?", userID, teamID).First(&member).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + return false, err + } + return member.Role == models.TeamRoleAdmin, nil +} + +// Checks if the user is an admin of a specific project (Internal Use) +func (s *PermissionService) isProjectAdmin(userID, projectID uint) (bool, error) { + var member models.ProjectMember + err := s.db.Where("user_id = ? AND project_id = ?", userID, projectID).First(&member).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + return false, err + } + return member.Role == models.ProjectRoleAdmin, nil +} + +// Checks if the user has permission to manage a team (e.g., edit info, add members) +// SysAdmin OR TeamAdmin of that team can manage the team +func (s *PermissionService) CanManageTeam(userID, teamID uint) (bool, error) { + if isAdmin, err := s.IsSysAdmin(userID); err != nil { + return false, err + } else if isAdmin { + return true, nil + } + + return s.isTeamAdmin(userID, teamID) +} + +// Checks if the user can access a team (e.g., view team content) +// SysAdmin OR any member of that team (Member/Admin) can access the team +func (s *PermissionService) CanAccessTeam(userID, teamID uint) (bool, error) { + if isAdmin, err := s.IsSysAdmin(userID); err != nil { + return false, err + } else if isAdmin { + return true, nil + } + + var count int64 + err := s.db.Model(&models.TeamMember{}). + Where("user_id = ? AND team_id = ?", userID, teamID). + Count(&count).Error + return count > 0, err +} + +// Checks if the user has permission to manage a project. +// SysAdmin OR TeamAdmin of the parent team OR ProjectAdmin of that project. +func (s *PermissionService) CanManageProject(userID, projectID uint) (bool, error) { + // Check SysAdmin + if isAdmin, err := s.IsSysAdmin(userID); err != nil { + return false, err + } else if isAdmin { + return true, nil + } + + // Retrieve the project to identify its parent TeamID + var project models.Project + if err := s.db.Select("team_id").First(&project, projectID).Error; err != nil { + return false, err + } + + // Check TeamAdmin of the parent team + if isTeamAdmin, err := s.isTeamAdmin(userID, project.TeamID); err != nil { + return false, err + } else if isTeamAdmin { + return true, nil + } + + return s.isProjectAdmin(userID, projectID) +} + +// Checks if the user can access a project. +// SysAdmin OR TeamAdmin OR Project Member +func (s *PermissionService) CanAccessProject(userID, projectID uint) (bool, error) { + if canManage, err := s.CanManageProject(userID, projectID); err != nil { + return false, err + } else if canManage { + return true, nil + } + + var count int64 + err := s.db.Model(&models.ProjectMember{}). + Where("user_id = ? AND project_id = ?", userID, projectID). + Count(&count).Error + return count > 0, err +} \ No newline at end of file diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index c9a31f2..cbe597a 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -20,8 +20,8 @@ const routes: RouteRecordRaw[] = [ component: () => import('@/views/user/RegisterView.vue') }, { - path: '/boards', - name: 'Boards', + path: '/projects/:projectId/boards', + name: 'ProjectBoards', component: () => import('@/views/dashboard/BoardListView.vue'), meta: { requiresAuth: true } }, diff --git a/frontend/src/services/board-api.ts b/frontend/src/services/board-api.ts index 6a677ed..13666c9 100644 --- a/frontend/src/services/board-api.ts +++ b/frontend/src/services/board-api.ts @@ -104,9 +104,9 @@ export class BoardApiService { } // 创建看板 - async createKanban(board: Omit): Promise> { + async createKanban(board: Omit, projectId: string): Promise> { try { - const response = await api.post('/boards', board) + const response = await api.post(`/projects/${projectId}/boards`, board) return response.data } catch (error: any) { return { diff --git a/frontend/src/stores/board.ts b/frontend/src/stores/board.ts index 56cfc5d..9ff6fba 100644 --- a/frontend/src/stores/board.ts +++ b/frontend/src/stores/board.ts @@ -60,12 +60,12 @@ export const useBoardStore = defineStore('board', () => { * 创建看板 * 使用统一的数据转换函数,提高代码复用性 */ - const addBoard = async (board: Omit) => { + const addBoard = async (board: Omit, projectId: string) => { loading.value = true error.value = null try { - const response = await boardApiService.createKanban(board) + const response = await boardApiService.createKanban(board, projectId) if (response.data) { const newBoard = transformBoardData(response.data) diff --git a/frontend/src/views/dashboard/BoardListView.vue b/frontend/src/views/dashboard/BoardListView.vue index 00efd7b..244c63b 100644 --- a/frontend/src/views/dashboard/BoardListView.vue +++ b/frontend/src/views/dashboard/BoardListView.vue @@ -142,7 +142,7 @@