119 lines
3.5 KiB
Go
119 lines
3.5 KiB
Go
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}})
|
||
|
|
}
|