Feat v0.9.3 team user roles
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-runners (pull_request) Has been skipped
CI/CD / e2e-smoke (pull_request) Has been skipped
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / test-go-pg (pull_request) Successful in 2m49s
CI/CD / test-sqlite (pull_request) Successful in 2m57s
CI/CD / build-and-deploy (pull_request) Successful in 1m19s
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-runners (pull_request) Has been skipped
CI/CD / e2e-smoke (pull_request) Has been skipped
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / test-go-pg (pull_request) Successful in 2m49s
CI/CD / test-sqlite (pull_request) Successful in 2m57s
CI/CD / build-and-deploy (pull_request) Successful in 1m19s
Promote team roles to a kernel primitive with many-to-many support. Users can now hold multiple roles within a team simultaneously. - Migration 016: team_user_roles table (both dialects) - 6 new TeamStore methods (AddUserRole, RemoveUserRole, ListUserRoles, GetMemberRoles, HasRole, RemoveAllUserRoles) - RequireRole() middleware with OR semantics and system admin bypass - 3 new handler endpoints for member role CRUD - Manifest requires_roles field (advisory for v0.9.3) - Starlark teams module: get_member_roles(), has_role() - Team-admin UI: role badge chips + assignment dropdown - Fixed pre-existing SDK auto-unwrap bug in loadRoles - 10 new tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -360,6 +360,11 @@ func (h *TeamHandler) RemoveMember(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up additional roles from team_user_roles
|
||||
if affectedUID != "" {
|
||||
_ = h.stores.Teams.RemoveAllUserRoles(c.Request.Context(), teamID, affectedUID)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
if affectedUID != "" {
|
||||
h.notifyAuthChanged(affectedUID, "team_member_removed")
|
||||
@@ -449,6 +454,106 @@ func (h *TeamHandler) ListTeamAuditActions(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"actions": actions})
|
||||
}
|
||||
|
||||
// ── Member User Roles (many-to-many) ─────────
|
||||
|
||||
// ListMemberRoles returns the full effective role set for a team member.
|
||||
// GET /api/v1/teams/:teamId/members/:memberId/roles
|
||||
func (h *TeamHandler) ListMemberRoles(c *gin.Context) {
|
||||
teamID := getTeamID(c)
|
||||
memberID := c.Param("memberId")
|
||||
|
||||
uid := h.memberUserID(c.Request.Context(), teamID, memberID)
|
||||
if uid == "" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "member not found"})
|
||||
return
|
||||
}
|
||||
|
||||
roles, err := h.stores.Teams.GetMemberRoles(c.Request.Context(), teamID, uid)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
|
||||
return
|
||||
}
|
||||
|
||||
// Also fetch the primary role for the response
|
||||
member, _ := h.stores.Teams.GetMember(c.Request.Context(), teamID, uid)
|
||||
primary := ""
|
||||
if member != nil {
|
||||
primary = member.Role
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": roles, "primary": primary})
|
||||
}
|
||||
|
||||
// AddMemberRole assigns an additional role to a team member.
|
||||
// POST /api/v1/teams/:teamId/members/:memberId/roles
|
||||
func (h *TeamHandler) AddMemberRole(c *gin.Context) {
|
||||
teamID := getTeamID(c)
|
||||
memberID := c.Param("memberId")
|
||||
|
||||
var req struct {
|
||||
Role string `json:"role" binding:"required,min=1,max=50"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateTeamRole(c.Request.Context(), h.stores, teamID, req.Role); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
uid := h.memberUserID(c.Request.Context(), teamID, memberID)
|
||||
if uid == "" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "member not found"})
|
||||
return
|
||||
}
|
||||
|
||||
assignedBy := getUserID(c)
|
||||
if err := h.stores.Teams.AddUserRole(c.Request.Context(), teamID, uid, req.Role, assignedBy); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to add role"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"ok": true})
|
||||
h.notifyAuthChanged(uid, "team_role_added")
|
||||
AuditLog(h.stores.Audit, c, "team.add_member_role", "team", teamID, map[string]interface{}{
|
||||
"member_id": memberID, "user_id": uid, "role": req.Role,
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveMemberRole removes an additional role from a team member.
|
||||
// DELETE /api/v1/teams/:teamId/members/:memberId/roles/:role
|
||||
func (h *TeamHandler) RemoveMemberRole(c *gin.Context) {
|
||||
teamID := getTeamID(c)
|
||||
memberID := c.Param("memberId")
|
||||
role := c.Param("role")
|
||||
|
||||
uid := h.memberUserID(c.Request.Context(), teamID, memberID)
|
||||
if uid == "" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "member not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Don't allow removing the primary role via this endpoint
|
||||
member, _ := h.stores.Teams.GetMember(c.Request.Context(), teamID, uid)
|
||||
if member != nil && member.Role == role {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "cannot remove primary role; use member update instead"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.stores.Teams.RemoveUserRole(c.Request.Context(), teamID, uid, role); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to remove role"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
h.notifyAuthChanged(uid, "team_role_removed")
|
||||
AuditLog(h.stores.Audit, c, "team.remove_member_role", "team", teamID, map[string]interface{}{
|
||||
"member_id": memberID, "user_id": uid, "role": role,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Team Roles API ─────────────────
|
||||
|
||||
// builtinRoles are always present in every team's role list.
|
||||
|
||||
Reference in New Issue
Block a user