6278cb1022
- 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
93 lines
2.4 KiB
Go
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
|
|
}
|