This commit is contained in:
2026-06-22 08:55:57 +08:00
parent dbb87823e8
commit 6ade6e8fa9
325 changed files with 41131 additions and 855 deletions
+218
View File
@@ -0,0 +1,218 @@
// 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))
}