feat(inbox): scope all inbox queries by workspace_id

Inbox items were previously queried only by recipient, which leaked data
across workspaces. All list/count/batch operations now filter by
workspace_id from the X-Workspace-ID header.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-03-29 17:42:45 +08:00
parent 42f72371bd
commit 4126073229
4 changed files with 75 additions and 35 deletions

View file

@ -91,6 +91,7 @@ func (h *Handler) ListInbox(w http.ResponseWriter, r *http.Request) {
if !ok {
return
}
workspaceID := r.Header.Get("X-Workspace-ID")
limit := 50
offset := 0
@ -106,6 +107,7 @@ func (h *Handler) ListInbox(w http.ResponseWriter, r *http.Request) {
}
items, err := h.Queries.ListInboxItems(r.Context(), db.ListInboxItemsParams{
WorkspaceID: parseUUID(workspaceID),
RecipientType: "member",
RecipientID: parseUUID(userID),
Limit: int32(limit),
@ -173,8 +175,10 @@ func (h *Handler) CountUnreadInbox(w http.ResponseWriter, r *http.Request) {
if !ok {
return
}
workspaceID := r.Header.Get("X-Workspace-ID")
count, err := h.Queries.CountUnreadInbox(r.Context(), db.CountUnreadInboxParams{
WorkspaceID: parseUUID(workspaceID),
RecipientType: "member",
RecipientID: parseUUID(userID),
})
@ -191,15 +195,18 @@ func (h *Handler) MarkAllInboxRead(w http.ResponseWriter, r *http.Request) {
if !ok {
return
}
workspaceID := r.Header.Get("X-Workspace-ID")
count, err := h.Queries.MarkAllInboxRead(r.Context(), parseUUID(userID))
count, err := h.Queries.MarkAllInboxRead(r.Context(), db.MarkAllInboxReadParams{
WorkspaceID: parseUUID(workspaceID),
RecipientID: parseUUID(userID),
})
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to mark all inbox read")
return
}
slog.Info("inbox: mark all read", append(logger.RequestAttrs(r), "user_id", userID, "count", count)...)
workspaceID := r.Header.Get("X-Workspace-ID")
h.publish(protocol.EventInboxBatchRead, workspaceID, "member", userID, map[string]any{
"recipient_id": userID,
"count": count,
@ -213,15 +220,18 @@ func (h *Handler) ArchiveAllInbox(w http.ResponseWriter, r *http.Request) {
if !ok {
return
}
workspaceID := r.Header.Get("X-Workspace-ID")
count, err := h.Queries.ArchiveAllInbox(r.Context(), parseUUID(userID))
count, err := h.Queries.ArchiveAllInbox(r.Context(), db.ArchiveAllInboxParams{
WorkspaceID: parseUUID(workspaceID),
RecipientID: parseUUID(userID),
})
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to archive all inbox")
return
}
slog.Info("inbox: archive all", append(logger.RequestAttrs(r), "user_id", userID, "count", count)...)
workspaceID := r.Header.Get("X-Workspace-ID")
h.publish(protocol.EventInboxBatchArchived, workspaceID, "member", userID, map[string]any{
"recipient_id": userID,
"count": count,
@ -235,15 +245,18 @@ func (h *Handler) ArchiveAllReadInbox(w http.ResponseWriter, r *http.Request) {
if !ok {
return
}
workspaceID := r.Header.Get("X-Workspace-ID")
count, err := h.Queries.ArchiveAllReadInbox(r.Context(), parseUUID(userID))
count, err := h.Queries.ArchiveAllReadInbox(r.Context(), db.ArchiveAllReadInboxParams{
WorkspaceID: parseUUID(workspaceID),
RecipientID: parseUUID(userID),
})
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to archive all read inbox")
return
}
slog.Info("inbox: archive all read", append(logger.RequestAttrs(r), "user_id", userID, "count", count)...)
workspaceID := r.Header.Get("X-Workspace-ID")
h.publish(protocol.EventInboxBatchArchived, workspaceID, "member", userID, map[string]any{
"recipient_id": userID,
"count": count,
@ -257,15 +270,18 @@ func (h *Handler) ArchiveCompletedInbox(w http.ResponseWriter, r *http.Request)
if !ok {
return
}
workspaceID := r.Header.Get("X-Workspace-ID")
count, err := h.Queries.ArchiveCompletedInbox(r.Context(), parseUUID(userID))
count, err := h.Queries.ArchiveCompletedInbox(r.Context(), db.ArchiveCompletedInboxParams{
WorkspaceID: parseUUID(workspaceID),
RecipientID: parseUUID(userID),
})
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to archive completed inbox")
return
}
slog.Info("inbox: archive completed", append(logger.RequestAttrs(r), "user_id", userID, "count", count)...)
workspaceID := r.Header.Get("X-Workspace-ID")
h.publish(protocol.EventInboxBatchArchived, workspaceID, "member", userID, map[string]any{
"recipient_id": userID,
"count": count,