Initial commit of Descrybe v2 without local scratch artifacts.

Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
2026-08-09 22:47:43 +02:00
commit 8580c996c3
1285 changed files with 325780 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
package httpapi
import (
"bytes"
"encoding/json"
"errors"
"io"
"log"
"net/http"
"regexp"
"strings"
"github.com/descrybe/descrybe-v2/apps/api/internal/i18n"
)
const maxJSONBodyBytes = 2 << 20 // 2 MiB
var errJSONBodyTooLarge = errors.New("request body too large")
var errJSONTrailingContent = errors.New("request body must contain a single JSON object")
// secretLikeRE matches common secret material that must never appear in logs.
var secretLikeRE = regexp.MustCompile(`(?i)(password|passwd|secret|api[_-]?key|token|authorization|bearer|sk_live|sk_test|whsec_)[^\s]{0,64}`)
func redactForLog(msg string) string {
if msg == "" {
return msg
}
return secretLikeRE.ReplaceAllStringFunc(msg, func(m string) string {
parts := strings.SplitN(m, "=", 2)
if len(parts) == 2 {
return parts[0] + "=[REDACTED]"
}
if i := strings.IndexByte(m, ':'); i > 0 && i < 24 {
return m[:i+1] + "[REDACTED]"
}
return "[REDACTED]"
})
}
func JSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
func Error(w http.ResponseWriter, status int, msg string) {
JSON(w, status, map[string]string{"error": PublicMessage(w, msg)})
}
// FieldError writes the usual public error string plus an additive field map:
//
// { "error": "...", "code": "...?", "fields": { "<field>": "..." } }
//
// `error` stays the localized human message. Optional `code` is a stable
// machine token (never translated). `fields` lets dashboards highlight inputs
// under any Accept-Language without English substring matching.
// Clients that only read `error` keep working (no BREAKING change).
func FieldError(w http.ResponseWriter, status int, msg string, code string, fields map[string]string) {
localized := PublicMessage(w, msg)
out := map[string]any{"error": localized}
if code != "" {
out["code"] = code
}
if len(fields) > 0 {
lf := make(map[string]string, len(fields))
for k, v := range fields {
if k == "" {
continue
}
text := v
if text == "" {
text = msg
}
lf[k] = PublicMessage(w, text)
}
if len(lf) > 0 {
out["fields"] = lf
}
}
JSON(w, status, out)
}
// CodedError writes the legacy public-API error envelope:
//
// { "error": { "code": "...", "message": "..." } }
//
// Used for /api/v1 API-key auth failures so clients migrating from Descrybe
// see the same shape as err(code, message) in the Next.js app.
// Code is never translated; message respects Accept-Language via Locale middleware.
func CodedError(w http.ResponseWriter, status int, code, message string) {
JSON(w, status, map[string]any{
"error": map[string]string{"code": code, "message": PublicMessage(w, message)},
})
}
// PublicMessage localizes a client-facing string for the request locale.
// Stable machine codes (password_not_set, maintenance, …) stay unchanged.
func PublicMessage(w http.ResponseWriter, msg string) string {
return i18n.T(localeOf(w), msg)
}
// LogAndError logs the real error server-side (secrets redacted) and returns a safe public message.
func LogAndError(w http.ResponseWriter, status int, publicMsg string, err error) {
if err != nil {
log.Printf("httpapi: %s: %s", publicMsg, redactForLog(err.Error()))
}
Error(w, status, publicMsg)
}
// ClientOrLog writes a known client message, or logs and returns publicFallback.
func ClientOrLog(w http.ResponseWriter, status int, publicFallback string, err error, clientMsg func(error) (string, bool)) {
if msg, ok := clientMsg(err); ok {
Error(w, status, msg)
return
}
LogAndError(w, status, publicFallback, err)
}
func DecodeJSON(r *http.Request, dst any) error {
return decodeJSON(r, dst, true)
}
// DecodeJSONAllowUnknown decodes JSON without DisallowUnknownFields.
// Used for legacy public process payloads that may include extra item keys.
func DecodeJSONAllowUnknown(r *http.Request, dst any) error {
return decodeJSON(r, dst, false)
}
func decodeJSON(r *http.Request, dst any, disallowUnknown bool) error {
defer r.Body.Close()
data, err := io.ReadAll(io.LimitReader(r.Body, maxJSONBodyBytes+1))
if err != nil {
return err
}
if len(data) > maxJSONBodyBytes {
return errJSONBodyTooLarge
}
dec := json.NewDecoder(bytes.NewReader(data))
if disallowUnknown {
dec.DisallowUnknownFields()
}
if err := dec.Decode(dst); err != nil {
return err
}
if err := dec.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
return errJSONTrailingContent
}
return nil
}
func DecodeJSONOptional(r *http.Request, dst any) error {
err := DecodeJSON(r, dst)
if errors.Is(err, io.EOF) {
return nil
}
return err
}