server/internal/handlers/notes.go
Rama main · réplica de lectura del repositorio original.
package handlers
import (
"encoding/json"
"errors"
"log"
"net/http"
"strconv"
"strings"
"time"
"unicode/utf8"
"blackbox/server/internal/auth"
"blackbox/server/internal/models"
"blackbox/shared/types"
"github.com/go-chi/chi/v5"
"github.com/oklog/ulid/v2"
"gorm.io/gorm"
)
const (
maxNoteLength = 2000
defaultNotesPage = 50
maxNotesPage = 100
maxNoteBodyBytes = 16 << 10
)
func CreateNote(database *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.ClaimsFromContext(r.Context())
if !ok {
writeError(w, http.StatusUnauthorized, "unauthorized")
return
}
entryID := chi.URLParam(r, "id")
var entry types.Entry
if err := database.First(&entry, "id = ?", entryID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
writeError(w, http.StatusNotFound, "entry not found")
return
}
writeError(w, http.StatusInternalServerError, "failed to fetch entry")
return
}
var body struct {
Content string `json:"content"`
}
if !decodeJSONBody(w, r, maxNoteBodyBytes, &body) {
return
}
content := strings.TrimSpace(body.Content)
if content == "" {
writeError(w, http.StatusBadRequest, "content is required")
return
}
if utf8.RuneCountInString(content) > maxNoteLength {
writeError(w, http.StatusBadRequest, "content exceeds max length")
return
}
note := models.EntryNote{
ID: ulid.Make().String(),
EntryID: entryID,
UserID: claims.UserID,
Username: claims.Username,
Content: content,
CreatedAt: time.Now().UTC(),
}
if err := database.Create(¬e).Error; err != nil {
writeError(w, http.StatusInternalServerError, "failed to save note")
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(note); err != nil {
log.Printf("CreateNote encode: %v", err)
}
}
}
func ListNotes(database *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
entryID := chi.URLParam(r, "id")
limit := defaultNotesPage
if rawLimit := r.URL.Query().Get("limit"); rawLimit != "" {
if parsedLimit, err := strconv.Atoi(rawLimit); err == nil && parsedLimit > 0 {
if parsedLimit > maxNotesPage {
limit = maxNotesPage
} else {
limit = parsedLimit
}
}
}
offset := 0
if rawOffset := r.URL.Query().Get("offset"); rawOffset != "" {
if parsedOffset, err := strconv.Atoi(rawOffset); err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
var notes []models.EntryNote
if err := database.
Where("entry_id = ?", entryID).
Order("created_at ASC").
Order("id ASC").
Offset(offset).
Limit(limit + 1).
Find(¬es).Error; err != nil {
writeError(w, http.StatusInternalServerError, "failed to fetch notes")
return
}
if notes == nil {
notes = []models.EntryNote{}
}
hasMore := len(notes) > limit
if hasMore {
notes = notes[:limit]
}
response := struct {
Notes []models.EntryNote `json:"notes"`
HasMore bool `json:"has_more"`
NextOffset int `json:"next_offset,omitempty"`
}{
Notes: notes,
HasMore: hasMore,
}
if hasMore {
response.NextOffset = offset + len(notes)
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Printf("ListNotes encode: %v", err)
}
}
}
func DeleteNote(database *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.ClaimsFromContext(r.Context())
if !ok {
writeError(w, http.StatusUnauthorized, "unauthorized")
return
}
noteID := chi.URLParam(r, "id")
result := database.Where("id = ? AND user_id = ?", noteID, claims.UserID).Delete(&models.EntryNote{})
if result.Error != nil {
writeError(w, http.StatusInternalServerError, "failed to delete note")
return
}
if result.RowsAffected == 0 {
var note models.EntryNote
if err := database.First(¬e, "id = ?", noteID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
writeError(w, http.StatusNotFound, "note not found")
return
}
writeError(w, http.StatusInternalServerError, "failed to fetch note")
return
}
writeError(w, http.StatusForbidden, "not your note")
return
}
w.WriteHeader(http.StatusNoContent)
}
}