mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 09:26:50 +08:00
* MUL-7016 route daemon workspace sync reads to replica Co-authored-by: multica-agent <[email protected]> * test(handler): remove redundant daemon read routing test Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]>
88 lines
2.8 KiB
Go
88 lines
2.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/multica-ai/multica/server/internal/dbreader"
|
|
"github.com/multica-ai/multica/server/internal/middleware"
|
|
db "github.com/multica-ai/multica/server/pkg/db/generated"
|
|
)
|
|
|
|
type DaemonWorkspaceResponse struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// ListDaemonWorkspaces returns the minimal workspace membership projection
|
|
// needed by local daemons. User-scoped PAT/JWT callers receive every workspace
|
|
// they belong to; workspace-scoped daemon tokens receive only their bound
|
|
// workspace. This reconciliation read is eventual-consistency safe: daemons
|
|
// repeat it on hints and timers, and downstream daemon APIs authorize their own
|
|
// requests instead of treating this response as an admission decision.
|
|
func (h *Handler) ListDaemonWorkspaces(w http.ResponseWriter, r *http.Request) {
|
|
var resp []DaemonWorkspaceResponse
|
|
if userID := requestUserID(r); userID != "" {
|
|
rows, err := dbreader.Read(
|
|
r.Context(),
|
|
h.ReadSelector,
|
|
dbreader.BusinessDaemonWorkspaces,
|
|
dbreader.EventualConsistency,
|
|
func(ctx context.Context, q *db.Queries) ([]db.ListDaemonWorkspacesRow, error) {
|
|
return q.ListDaemonWorkspaces(ctx, parseUUID(userID))
|
|
},
|
|
)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to list daemon workspaces")
|
|
return
|
|
}
|
|
resp = make([]DaemonWorkspaceResponse, len(rows))
|
|
for i, row := range rows {
|
|
resp[i] = daemonWorkspaceToResponse(row.ID, row.Name)
|
|
}
|
|
} else {
|
|
workspaceID := middleware.DaemonWorkspaceIDFromContext(r.Context())
|
|
if workspaceID == "" {
|
|
writeError(w, http.StatusUnauthorized, "daemon workspace identity required")
|
|
return
|
|
}
|
|
row, err := dbreader.Read(
|
|
r.Context(),
|
|
h.ReadSelector,
|
|
dbreader.BusinessDaemonWorkspaces,
|
|
dbreader.EventualConsistency,
|
|
func(ctx context.Context, q *db.Queries) (db.GetDaemonWorkspaceRow, error) {
|
|
return q.GetDaemonWorkspace(ctx, parseUUID(workspaceID))
|
|
},
|
|
)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "workspace not found")
|
|
return
|
|
}
|
|
resp = []DaemonWorkspaceResponse{daemonWorkspaceToResponse(row.ID, row.Name)}
|
|
}
|
|
|
|
etag := daemonWorkspacesETag(resp)
|
|
w.Header().Set("Cache-Control", "private, no-cache")
|
|
w.Header().Set("ETag", etag)
|
|
if r.Header.Get("If-None-Match") == etag {
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func daemonWorkspaceToResponse(id pgtype.UUID, name string) DaemonWorkspaceResponse {
|
|
return DaemonWorkspaceResponse{ID: uuidToString(id), Name: name}
|
|
}
|
|
|
|
func daemonWorkspacesETag(workspaces []DaemonWorkspaceResponse) string {
|
|
data, _ := json.Marshal(workspaces)
|
|
sum := sha256.Sum256(data)
|
|
return fmt.Sprintf(`W/"%x"`, sum)
|
|
}
|