Files
nib-v1/internal/api/helpers.go
T
lerko 6278cb1022 fix: code principles audit — correctness, security, testability
- Add rows.Err() checks after all scan loops (entities, tags, resolve)
- Surface time.Parse errors instead of silently discarding
- Extract entityRow scan helper to eliminate Get/List duplication
- Cap request body at 1MB via MaxBytesReader
- Stop leaking internal errors to API clients (log server-side only)
- Block javascript: URIs in link card open button (XSS)
- Fix all go vet failures in api_test.go (unchecked http errors)
- Add tests for display package, generateCardData, absorb-source-card
- Run go mod tidy to fix direct/indirect dep markers
2026-05-14 17:41:30 -04:00

93 lines
2.4 KiB
Go

package api
import (
"encoding/json"
"log"
"net/http"
"time"
"github.com/lerko/nib/internal/db"
)
const maxBodySize = 1 << 20 // 1 MB
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
}
type EntityResponse struct {
ID string `json:"id"`
CreatedAt string `json:"created_at"`
ModifiedAt string `json:"modified_at"`
Body string `json:"body"`
Glyph string `json:"glyph"`
TimeAnchor *string `json:"time_anchor"`
CompletedAt *string `json:"completed_at"`
Pinned bool `json:"pinned"`
DeletedAt *string `json:"deleted_at"`
Tags []string `json:"tags"`
CardType *string `json:"card_type"`
CardData *string `json:"card_data"`
UseCount int `json:"use_count"`
LastUsedAt *string `json:"last_used_at"`
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, code, message string) {
writeJSON(w, status, ErrorResponse{Error: code, Message: message})
}
func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) bool {
r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
if err := json.NewDecoder(r.Body).Decode(dst); err != nil {
writeError(w, http.StatusBadRequest, "invalid_input", "malformed JSON: "+err.Error())
return false
}
return true
}
func writeInternalError(w http.ResponseWriter, err error) {
log.Printf("internal error: %v", err)
writeError(w, http.StatusInternalServerError, "internal", "internal server error")
}
func entityToResponse(e *db.Entity) EntityResponse {
resp := EntityResponse{
ID: e.ID,
CreatedAt: e.CreatedAt.Format(time.RFC3339),
ModifiedAt: e.ModifiedAt.Format(time.RFC3339),
Body: e.Body,
Glyph: string(e.Glyph),
Pinned: e.Pinned,
Tags: e.Tags,
UseCount: e.UseCount,
}
if resp.Tags == nil {
resp.Tags = []string{}
}
resp.TimeAnchor = e.TimeAnchor
resp.CompletedAt = formatTimeRespPtr(e.CompletedAt)
resp.DeletedAt = formatTimeRespPtr(e.DeletedAt)
resp.LastUsedAt = formatTimeRespPtr(e.LastUsedAt)
if e.CardType != nil {
s := string(*e.CardType)
resp.CardType = &s
}
resp.CardData = e.CardData
return resp
}
func formatTimeRespPtr(t *time.Time) *string {
if t == nil {
return nil
}
s := t.Format(time.RFC3339)
return &s
}