// handler.go exposes the change-request HTTP endpoints under middleware.Auth, // mirroring workspace/handler.go. Routes: // // POST /api/v1/workspaces/{wsID}/change-requests create // GET /api/v1/workspaces/{wsID}/change-requests list // GET /api/v1/change-requests/{id} get // POST /api/v1/change-requests/{id}/review review (human approve/reject) // POST /api/v1/change-requests/{id}/merge gated merge // POST /api/v1/change-requests/{id}/sync refresh status from host package changerequest import ( "context" "encoding/json" "net/http" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/yan1h/agent-coding-workflow/internal/infra/errs" httpx "github.com/yan1h/agent-coding-workflow/internal/transport/http" "github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware" ) // AdminLookup resolves is_admin for a user (narrow interface, same as // workspace.AdminLookup so the userAdminAdapter can be reused). type AdminLookup interface { IsAdmin(ctx context.Context, id uuid.UUID) (bool, error) } // Handler holds the change-request service + auth deps. type Handler struct { svc Service resolver middleware.SessionResolver users AdminLookup } // NewHandler constructs the Handler. func NewHandler(svc Service, resolver middleware.SessionResolver, users AdminLookup) *Handler { return &Handler{svc: svc, resolver: resolver, users: users} } // Mount registers the change-request routes on r under the Auth middleware. func (h *Handler) Mount(r chi.Router) { auth := middleware.Auth(h.resolver, middleware.AuthOptions{}) r.With(auth).Route("/api/v1/workspaces/{wsID}/change-requests", func(r chi.Router) { r.Post("/", h.create) r.Get("/", h.list) }) r.With(auth).Route("/api/v1/change-requests/{id}", func(r chi.Router) { r.Get("/", h.get) r.Post("/review", h.review) r.Post("/merge", h.merge) r.Post("/sync", h.sync) }) } func (h *Handler) caller(r *http.Request) (Caller, error) { uid, ok := middleware.UserIDFromContext(r.Context()) if !ok { return Caller{}, errs.New(errs.CodeUnauthorized, "unauthenticated") } isAdmin, err := h.users.IsAdmin(r.Context(), uid) if err != nil { return Caller{}, err } return Caller{UserID: uid, IsAdmin: isAdmin}, nil } func writeJSON(w http.ResponseWriter, status int, body any) { httpx.WriteJSON(w, status, body) } func writeErr(w http.ResponseWriter, r *http.Request, err error) { httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), err) } func parseUUID(s string) (uuid.UUID, error) { id, err := uuid.Parse(s) if err != nil { return uuid.Nil, errs.Wrap(err, errs.CodeInvalidInput, "invalid uuid") } return id, nil } func (h *Handler) create(w http.ResponseWriter, r *http.Request) { c, err := h.caller(r) if err != nil { writeErr(w, r, err) return } wsID, err := parseUUID(chi.URLParam(r, "wsID")) if err != nil { writeErr(w, r, err) return } var req createReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode body")) return } cr, err := h.svc.Create(r.Context(), c, wsID, CreateInput{ SourceBranch: req.SourceBranch, TargetBranch: req.TargetBranch, Title: req.Title, Description: req.Description, RequirementID: req.RequirementID, IssueID: req.IssueID, }) if err != nil { writeErr(w, r, err) return } writeJSON(w, http.StatusCreated, toResp(cr)) } func (h *Handler) list(w http.ResponseWriter, r *http.Request) { c, err := h.caller(r) if err != nil { writeErr(w, r, err) return } wsID, err := parseUUID(chi.URLParam(r, "wsID")) if err != nil { writeErr(w, r, err) return } out, err := h.svc.ListByWorkspace(r.Context(), c, wsID) if err != nil { writeErr(w, r, err) return } resp := make([]changeRequestResp, 0, len(out)) for _, cr := range out { resp = append(resp, toResp(cr)) } writeJSON(w, http.StatusOK, resp) } func (h *Handler) get(w http.ResponseWriter, r *http.Request) { c, err := h.caller(r) if err != nil { writeErr(w, r, err) return } id, err := parseUUID(chi.URLParam(r, "id")) if err != nil { writeErr(w, r, err) return } cr, err := h.svc.Get(r.Context(), c, id) if err != nil { writeErr(w, r, err) return } writeJSON(w, http.StatusOK, toResp(cr)) } func (h *Handler) review(w http.ResponseWriter, r *http.Request) { c, err := h.caller(r) if err != nil { writeErr(w, r, err) return } id, err := parseUUID(chi.URLParam(r, "id")) if err != nil { writeErr(w, r, err) return } var req reviewReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode body")) return } cr, err := h.svc.Review(r.Context(), c, id, Verdict(req.Verdict), req.Note) if err != nil { writeErr(w, r, err) return } writeJSON(w, http.StatusOK, toResp(cr)) } func (h *Handler) merge(w http.ResponseWriter, r *http.Request) { c, err := h.caller(r) if err != nil { writeErr(w, r, err) return } id, err := parseUUID(chi.URLParam(r, "id")) if err != nil { writeErr(w, r, err) return } var req mergeReq // merge body is optional (method default) _ = json.NewDecoder(r.Body).Decode(&req) cr, err := h.svc.Merge(r.Context(), c, id, req.Method) if err != nil { writeErr(w, r, err) return } writeJSON(w, http.StatusOK, toResp(cr)) } func (h *Handler) sync(w http.ResponseWriter, r *http.Request) { c, err := h.caller(r) if err != nil { writeErr(w, r, err) return } id, err := parseUUID(chi.URLParam(r, "id")) if err != nil { writeErr(w, r, err) return } cr, err := h.svc.SyncStatus(r.Context(), c, id) if err != nil { writeErr(w, r, err) return } writeJSON(w, http.StatusOK, toResp(cr)) }