Files
story/internal/memoir/http.go
T

119 lines
3.5 KiB
Go
Raw Normal View History

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}})
}