Root cause: backend hardcoded UserType="member" in subscribe/unsubscribe handlers, so unsubscribing an agent actually tried to delete a member record (no-op), and the WS event broadcast wrong user_type causing other subscribers' UI to break. - Backend: accept optional user_type in subscribe/unsubscribe request body - Frontend: pass userType through API client to backend - Fixes: clicking agent checkbox no longer affects member subscriptions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
146 lines
4.1 KiB
Go
146 lines
4.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
db "github.com/multica-ai/multica/server/pkg/db/generated"
|
|
"github.com/multica-ai/multica/server/pkg/protocol"
|
|
)
|
|
|
|
// SubscriberResponse is the JSON shape returned for each issue subscriber.
|
|
type SubscriberResponse struct {
|
|
IssueID string `json:"issue_id"`
|
|
UserType string `json:"user_type"`
|
|
UserID string `json:"user_id"`
|
|
Reason string `json:"reason"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
func subscriberToResponse(s db.IssueSubscriber) SubscriberResponse {
|
|
return SubscriberResponse{
|
|
IssueID: uuidToString(s.IssueID),
|
|
UserType: s.UserType,
|
|
UserID: uuidToString(s.UserID),
|
|
Reason: s.Reason,
|
|
CreatedAt: timestampToString(s.CreatedAt),
|
|
}
|
|
}
|
|
|
|
// ListIssueSubscribers returns all subscribers for an issue.
|
|
func (h *Handler) ListIssueSubscribers(w http.ResponseWriter, r *http.Request) {
|
|
issueID := chi.URLParam(r, "id")
|
|
issue, ok := h.loadIssueForUser(w, r, issueID)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
subscribers, err := h.Queries.ListIssueSubscribers(r.Context(), issue.ID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to list subscribers")
|
|
return
|
|
}
|
|
|
|
resp := make([]SubscriberResponse, len(subscribers))
|
|
for i, s := range subscribers {
|
|
resp[i] = subscriberToResponse(s)
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
// SubscribeToIssue subscribes a user to an issue with reason "manual".
|
|
// If request body contains user_id, subscribes that user; otherwise subscribes the caller.
|
|
func (h *Handler) SubscribeToIssue(w http.ResponseWriter, r *http.Request) {
|
|
issueID := chi.URLParam(r, "id")
|
|
issue, ok := h.loadIssueForUser(w, r, issueID)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Default to current user as member; allow specifying another user/agent
|
|
targetUserID := requestUserID(r)
|
|
targetUserType := "member"
|
|
var req struct {
|
|
UserID *string `json:"user_id"`
|
|
UserType *string `json:"user_type"`
|
|
}
|
|
if r.Body != nil {
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
}
|
|
if req.UserID != nil && *req.UserID != "" {
|
|
targetUserID = *req.UserID
|
|
}
|
|
if req.UserType != nil && *req.UserType != "" {
|
|
targetUserType = *req.UserType
|
|
}
|
|
|
|
err := h.Queries.AddIssueSubscriber(r.Context(), db.AddIssueSubscriberParams{
|
|
IssueID: issue.ID,
|
|
UserType: targetUserType,
|
|
UserID: parseUUID(targetUserID),
|
|
Reason: "manual",
|
|
})
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to subscribe")
|
|
return
|
|
}
|
|
|
|
workspaceID := uuidToString(issue.WorkspaceID)
|
|
callerID := requestUserID(r)
|
|
h.publish(protocol.EventSubscriberAdded, workspaceID, "member", callerID, map[string]any{
|
|
"issue_id": issueID,
|
|
"user_type": targetUserType,
|
|
"user_id": targetUserID,
|
|
"reason": "manual",
|
|
})
|
|
|
|
writeJSON(w, http.StatusOK, map[string]bool{"subscribed": true})
|
|
}
|
|
|
|
// UnsubscribeFromIssue removes a user's subscription from an issue.
|
|
// If request body contains user_id, unsubscribes that user; otherwise unsubscribes the caller.
|
|
func (h *Handler) UnsubscribeFromIssue(w http.ResponseWriter, r *http.Request) {
|
|
issueID := chi.URLParam(r, "id")
|
|
issue, ok := h.loadIssueForUser(w, r, issueID)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
targetUserID := requestUserID(r)
|
|
targetUserType := "member"
|
|
var req struct {
|
|
UserID *string `json:"user_id"`
|
|
UserType *string `json:"user_type"`
|
|
}
|
|
if r.Body != nil {
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
}
|
|
if req.UserID != nil && *req.UserID != "" {
|
|
targetUserID = *req.UserID
|
|
}
|
|
if req.UserType != nil && *req.UserType != "" {
|
|
targetUserType = *req.UserType
|
|
}
|
|
|
|
err := h.Queries.RemoveIssueSubscriber(r.Context(), db.RemoveIssueSubscriberParams{
|
|
IssueID: issue.ID,
|
|
UserType: targetUserType,
|
|
UserID: parseUUID(targetUserID),
|
|
})
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to unsubscribe")
|
|
return
|
|
}
|
|
|
|
workspaceID := uuidToString(issue.WorkspaceID)
|
|
callerID := requestUserID(r)
|
|
h.publish(protocol.EventSubscriberRemoved, workspaceID, "member", callerID, map[string]any{
|
|
"issue_id": issueID,
|
|
"user_type": targetUserType,
|
|
"user_id": targetUserID,
|
|
})
|
|
|
|
writeJSON(w, http.StatusOK, map[string]bool{"subscribed": false})
|
|
}
|