Changeset 0.27.4 (#171)
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/database"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/taskutil"
|
||||
@@ -50,6 +51,33 @@ func (h *TaskHandler) ListAll(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"data": tasks})
|
||||
}
|
||||
|
||||
// ListTeamTasks returns tasks scoped to the given team.
|
||||
// Any team member can view; RequireTeamMember middleware enforces access.
|
||||
// GET /api/v1/teams/:teamId/tasks
|
||||
func (h *TaskHandler) ListTeamTasks(c *gin.Context) {
|
||||
teamID := c.Param("teamId")
|
||||
tasks, err := h.stores.Tasks.ListByTeam(c.Request.Context(), teamID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list team tasks"})
|
||||
return
|
||||
}
|
||||
if tasks == nil {
|
||||
tasks = []models.Task{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": tasks})
|
||||
}
|
||||
|
||||
// CreateTeamTask creates a task scoped to the team.
|
||||
// Team admins only (RequireTeamAdmin middleware).
|
||||
// POST /api/v1/teams/:teamId/tasks
|
||||
func (h *TaskHandler) CreateTeamTask(c *gin.Context) {
|
||||
teamID := c.Param("teamId")
|
||||
// Inject team context, then delegate to Create
|
||||
c.Set("force_team_id", teamID)
|
||||
c.Set("force_scope", "team")
|
||||
h.Create(c)
|
||||
}
|
||||
|
||||
// Create creates a new task.
|
||||
// POST /api/v1/tasks
|
||||
func (h *TaskHandler) Create(c *gin.Context) {
|
||||
@@ -70,6 +98,16 @@ func (h *TaskHandler) Create(c *gin.Context) {
|
||||
|
||||
t.OwnerID = c.GetString("user_id")
|
||||
|
||||
// v0.27.5: Team task context override (set by CreateTeamTask)
|
||||
if forceTeam, ok := c.Get("force_team_id"); ok {
|
||||
teamID := forceTeam.(string)
|
||||
t.TeamID = &teamID
|
||||
t.Scope = "team"
|
||||
}
|
||||
if forceScope, ok := c.Get("force_scope"); ok {
|
||||
t.Scope = forceScope.(string)
|
||||
}
|
||||
|
||||
// v0.27.2: Personal task check — non-admin users need tasks.allow_personal
|
||||
if t.Scope == "personal" || t.Scope == "" {
|
||||
if c.GetString("role") != "admin" && !taskCfg.AllowPersonal {
|
||||
@@ -143,6 +181,45 @@ func (h *TaskHandler) Create(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, t)
|
||||
}
|
||||
|
||||
// canAccessTask returns true if the user can view this task.
|
||||
// Access: owner, system admin, or team member (for team-scoped tasks).
|
||||
func (h *TaskHandler) canAccessTask(c *gin.Context, t *models.Task) bool {
|
||||
if c.GetString("role") == "admin" {
|
||||
return true
|
||||
}
|
||||
if t.OwnerID == c.GetString("user_id") {
|
||||
return true
|
||||
}
|
||||
// Team members can view team-scoped tasks
|
||||
if t.Scope == "team" && t.TeamID != nil {
|
||||
var exists bool
|
||||
database.DB.QueryRow(database.Q(
|
||||
`SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)`,
|
||||
), *t.TeamID, c.GetString("user_id")).Scan(&exists)
|
||||
return exists
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// canMutateTask returns true if the user can edit/delete/run this task.
|
||||
// Mutation: owner, system admin, or team admin (for team-scoped tasks).
|
||||
func (h *TaskHandler) canMutateTask(c *gin.Context, t *models.Task) bool {
|
||||
if c.GetString("role") == "admin" {
|
||||
return true
|
||||
}
|
||||
if t.OwnerID == c.GetString("user_id") {
|
||||
return true
|
||||
}
|
||||
if t.Scope == "team" && t.TeamID != nil {
|
||||
var teamRole string
|
||||
database.DB.QueryRow(database.Q(
|
||||
`SELECT role FROM team_members WHERE team_id = $1 AND user_id = $2`,
|
||||
), *t.TeamID, c.GetString("user_id")).Scan(&teamRole)
|
||||
return teamRole == "admin"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Get returns a single task.
|
||||
// GET /api/v1/tasks/:id
|
||||
func (h *TaskHandler) Get(c *gin.Context) {
|
||||
@@ -152,8 +229,7 @@ func (h *TaskHandler) Get(c *gin.Context) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
|
||||
return
|
||||
}
|
||||
// Ownership check (admin bypasses)
|
||||
if c.GetString("role") != "admin" && t.OwnerID != c.GetString("user_id") {
|
||||
if !h.canAccessTask(c, t) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
|
||||
return
|
||||
}
|
||||
@@ -172,7 +248,7 @@ func (h *TaskHandler) Update(c *gin.Context) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
|
||||
return
|
||||
}
|
||||
if c.GetString("role") != "admin" && t.OwnerID != c.GetString("user_id") {
|
||||
if !h.canMutateTask(c, t) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
|
||||
return
|
||||
}
|
||||
@@ -220,7 +296,7 @@ func (h *TaskHandler) Delete(c *gin.Context) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
|
||||
return
|
||||
}
|
||||
if c.GetString("role") != "admin" && t.OwnerID != c.GetString("user_id") {
|
||||
if !h.canMutateTask(c, t) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
|
||||
return
|
||||
}
|
||||
@@ -236,7 +312,20 @@ func (h *TaskHandler) Delete(c *gin.Context) {
|
||||
// GET /api/v1/tasks/:id/runs
|
||||
func (h *TaskHandler) ListRuns(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
runs, err := h.stores.Tasks.ListRuns(c.Request.Context(), id, 50)
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// Access check (v0.27.5)
|
||||
t, err := h.stores.Tasks.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
|
||||
return
|
||||
}
|
||||
if !h.canAccessTask(c, t) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
|
||||
return
|
||||
}
|
||||
|
||||
runs, err := h.stores.Tasks.ListRuns(ctx, id, 50)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list runs"})
|
||||
return
|
||||
@@ -265,7 +354,7 @@ func (h *TaskHandler) RunNow(c *gin.Context) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
|
||||
return
|
||||
}
|
||||
if c.GetString("role") != "admin" && t.OwnerID != c.GetString("user_id") {
|
||||
if !h.canMutateTask(c, t) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
|
||||
return
|
||||
}
|
||||
@@ -297,7 +386,7 @@ func (h *TaskHandler) KillRun(c *gin.Context) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
|
||||
return
|
||||
}
|
||||
if c.GetString("role") != "admin" && t.OwnerID != c.GetString("user_id") {
|
||||
if !h.canMutateTask(c, t) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user