package memoir import ( "data-terminal-cloud/internal/identity" "encoding/json" "errors" "github.com/jackc/pgx/v5" "io" "net/http" ) func (s *Service) RegisterRoutes(mux *http.ServeMux, wrap func(http.Handler) http.Handler) { for _, op := range []string{"query", "generate", "import"} { mux.Handle("POST /api/v1/memoirs/"+op, wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { p, ok := identity.PrincipalFromContext(r.Context()) if !ok { write(w, nil, errors.New("AUTHENTICATION_REQUIRED")) return } body, e := io.ReadAll(io.LimitReader(r.Body, 200001)) if e != nil || len(body) > 200000 { write(w, nil, errors.New("MEMOIR_IMPORT_INVALID")) return } if op == "import" { var in ImportInput if e = json.Unmarshal(body, &in); e != nil { write(w, nil, errors.New("MEMOIR_IMPORT_INVALID")) return } id, e := s.Import(r.Context(), p, in) write(w, map[string]string{"import_id": id}, e) return } var in struct { ElderID string `json:"elder_id"` } if json.Unmarshal(body, &in) != nil { write(w, nil, errors.New("MEMOIR_IMPORT_INVALID")) return } if op == "query" { v, e := s.Query(r.Context(), p, in.ElderID) write(w, v, e) } else { id, e := s.Enqueue(r.Context(), p, in.ElderID) write(w, map[string]string{"job_id": id}, e) } }))) } mux.Handle("GET /api/v1/memoirs/illustrations/{id}", wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { p, ok := identity.PrincipalFromContext(r.Context()) if !ok { write(w, nil, errors.New("AUTHENTICATION_REQUIRED")) return } var elder, digest string e := s.Pool.QueryRow(r.Context(), `SELECT sp.public_id::text,md.source_digest FROM memoir_illustration mi JOIN memoir_document md ON md.id=mi.document_id JOIN subject_profile sp ON sp.id=md.subject_id WHERE mi.id=$1::uuid`, r.PathValue("id")).Scan(&elder, &digest) if e != nil { write(w, nil, errors.New("MEMOIR_NOT_FOUND")) return } a, e := s.access(r.Context(), s.Pool, p, elder, false) if e != nil { write(w, nil, e) return } _, current, e := s.sources(r.Context(), s.Pool, a) if e != nil || current != digest { write(w, nil, ErrStale) return } var b []byte e = s.Pool.QueryRow(r.Context(), `SELECT content FROM memoir_illustration WHERE id=$1::uuid`, r.PathValue("id")).Scan(&b) if errors.Is(e, pgx.ErrNoRows) { write(w, nil, errors.New("MEMOIR_NOT_FOUND")) return } if e != nil { write(w, nil, e) return } w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Cache-Control", "private, no-store") w.Header().Set("X-Content-Type-Options", "nosniff") w.Write(b) }))) } func write(w http.ResponseWriter, v any, e error) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Cache-Control", "no-store") if e == nil { json.NewEncoder(w).Encode(map[string]any{"ok": true, "data": v}) return } code := e.Error() status := 400 var typed interface{ HTTPStatus() int } if errors.As(e, &typed) { status = typed.HTTPStatus() } switch code { case "AUTHENTICATION_REQUIRED": status = 401 case "MEMOIR_CONSENT_REQUIRED", "TRANSCRIPT_ACCESS_DENIED": status = 403 case "MEMOIR_NOT_FOUND": status = 404 case "MEMOIR_SOURCE_CHANGED", "MEMOIR_IMPORT_CONFLICT": status = 409 } if len(code) > 80 || status == 503 { code = "MEMOIR_SERVICE_UNAVAILABLE" status = 503 } w.WriteHeader(status) json.NewEncoder(w).Encode(map[string]any{"ok": false, "code": code, "error": map[string]any{"code": code, "message": code, "retryable": status == 503}}) }