package main import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "net/http" "os" "strings" "time" ) type DiagramApproval struct { Reviewer string `json:"reviewer"` At string `json:"at"` Note string `json:"note"` Revision int `json:"revision"` SourceHash string `json:"source_hash"` DiagramHash string `json:"diagram_hash"` } type UseCaseApprovalStatus struct { Approved bool `json:"approved"` State string `json:"state"` Revision int `json:"revision"` SourceHash string `json:"source_hash"` Latest *DiagramApproval `json:"latest,omitempty"` History []DiagramApproval `json:"history"` Error string `json:"error,omitempty"` } func digestJSON(v any) string { b, _ := json.Marshal(v) sum := sha256.Sum256(b) return hex.EncodeToString(sum[:]) } func sourceDigest(u UseCase) string { return digestJSON(struct { ID string Source UseCase }{u.ID, u}) } func approvalForDocument(u UseCase, doc diagramDocument) UseCaseApprovalStatus { s := UseCaseApprovalStatus{State: "pending", Revision: doc.Revision, SourceHash: sourceDigest(u), History: doc.Approvals} if s.History == nil { s.History = []DiagramApproval{} } if len(doc.Approvals) > 0 { latest := doc.Approvals[len(doc.Approvals)-1] s.Latest = &latest s.State = "stale" if latest.Revision == doc.Revision && latest.SourceHash == s.SourceHash && latest.DiagramHash == digestJSON(doc.Diagram) { s.Approved = true s.State = "approved" } } return s } func (ds *diagramStore) approvalStatus(u UseCase) UseCaseApprovalStatus { ds.mu.Lock() defer ds.mu.Unlock() doc, err := ds.readDocument(u.ID) if errors.Is(err, os.ErrNotExist) { doc.Diagram = draftDiagram(u) err = nil } s := approvalForDocument(u, doc) if err != nil { s.Approved = false s.State = "unavailable" s.Error = "Unable to read approval record" } return s } func (ds *diagramStore) approve(u UseCase, expected int, sourceHash, reviewer, note string, confirmed bool) (UseCaseApprovalStatus, error) { ds.mu.Lock() defer ds.mu.Unlock() reviewer = strings.TrimSpace(reviewer) note = strings.TrimSpace(note) if !confirmed || reviewer == "" || len(reviewer) > 200 || len(note) > 4000 { return UseCaseApprovalStatus{}, errors.New("reviewer and explicit confirmation required") } doc, err := ds.readDocument(u.ID) if errors.Is(err, os.ErrNotExist) { doc.Diagram = draftDiagram(u) err = nil } if err != nil { return UseCaseApprovalStatus{}, err } if doc.Revision != expected || sourceHash != sourceDigest(u) { return UseCaseApprovalStatus{}, errDiagramConflict } doc.Approvals = append(doc.Approvals, DiagramApproval{Reviewer: reviewer, At: time.Now().In(ict).Format(time.RFC3339), Note: note, Revision: doc.Revision, SourceHash: sourceHash, DiagramHash: digestJSON(doc.Diagram)}) if err = ds.writeDocument(u.ID, doc); err != nil { return UseCaseApprovalStatus{}, err } return approvalForDocument(u, doc), nil } func registerUseCaseApproval(mux *http.ServeMux, ds *diagramStore) { mux.HandleFunc("GET /api/use-cases/{uid}/approval", func(w http.ResponseWriter, r *http.Request) { u, ok := useCaseForRequest(w, r) if !ok { return } s := ds.approvalStatus(u) if s.Error != "" { http.Error(w, s.Error, 500) return } diagramResponse(w, s, nil) }) mux.HandleFunc("POST /api/use-cases/{uid}/approval", func(w http.ResponseWriter, r *http.Request) { if !sameOrigin(r) { http.Error(w, "Forbidden", 403) return } u, ok := useCaseForRequest(w, r) if !ok { return } var sub struct { Revision int `json:"revision"` SourceHash string `json:"source_hash"` Reviewer string `json:"reviewer"` Note string `json:"note"` Confirmed bool `json:"confirmed"` } if !decodeDiagramRequest(w, r, &sub) { return } if sub.Revision < 0 || strings.TrimSpace(sub.Reviewer) == "" || len(sub.Reviewer) > 200 || len(sub.Note) > 4000 || !sub.Confirmed { http.Error(w, "Reviewer name and confirmation are required", 400) return } s, err := ds.approve(u, sub.Revision, sub.SourceHash, sub.Reviewer, sub.Note, sub.Confirmed) diagramResponse(w, s, err) }) }