Feat v0.3.4 team roles + multi-party validation (#18)

Custom team roles: removed CHECK constraint on team_members.role,
roles stored in teams.settings, roles API, stage_config.required_role
enforced on claim. Multi-party signoff: workflow_signoffs table,
validation gate in advanceInternal, SubmitSignoff engine method,
signoff HTTP API. Frontend: dynamic role management, stage config
validation UI, signoff panel. Design docs for extension lifecycle
and trigger composition. 20 store tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-28 00:22:04 +00:00
parent dba718b914
commit 748da6c2b4
22 changed files with 952 additions and 19 deletions

View File

@@ -30,12 +30,28 @@ func NewWorkflowAssignmentHandler(engine *workflow.Engine, stores store.Stores)
func (h *WorkflowAssignmentHandler) Claim(c *gin.Context) {
userID := c.GetString("user_id")
assignmentID := c.Param("id")
ctx := c.Request.Context()
if err := h.stores.Workflows.ClaimAssignment(c.Request.Context(), assignmentID, userID); err != nil {
// Claim first, then verify role. Rollback if role check fails.
if err := h.stores.Workflows.ClaimAssignment(ctx, assignmentID, userID); err != nil {
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
return
}
// Role check: find the assignment in user's claimed list and verify role
claimed, _ := h.stores.Workflows.ListAssignmentsByUser(ctx, userID, models.AssignmentStatusClaimed)
for _, a := range claimed {
if a.ID == assignmentID {
if rerr := workflow.CheckClaimRole(ctx, h.stores, &a, userID); rerr != nil {
// Rollback: unclaim
h.stores.Workflows.UnclaimAssignment(ctx, assignmentID)
c.JSON(http.StatusForbidden, gin.H{"error": rerr.Error()})
return
}
break
}
}
c.JSON(http.StatusOK, gin.H{"claimed": true})
}