package httpapi import ( "errors" "net/http" "strconv" "pure-note/internal/store" ) // handleHealth GET /api/health。 func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { if err := s.st.DB().Ping(); err != nil { writeError(w, http.StatusInternalServerError, "db_unavailable", "数据库不可用") return } writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } // handleMe GET /api/me:匿名 {authenticated:false};已认证 {authenticated:true, csrf_token}。 func (s *Server) handleMe(w http.ResponseWriter, r *http.Request) { if sess, ok := s.sessionFrom(r); ok { s.maybeRotate(w, sess) writeJSON(w, http.StatusOK, map[string]any{"authenticated": true, "csrf_token": sess.CSRFToken}) return } writeJSON(w, http.StatusOK, map[string]any{"authenticated": false}) } // handleSiteInfo GET /api/site:站点设置白名单视图(标题/副标题/每页条数)。 // 永不包含 admin_password_hash(§9.3)。 func (s *Server) handleSiteInfo(w http.ResponseWriter, r *http.Request) { ss, err := s.st.GetSiteSettings() if err != nil { writeError(w, http.StatusInternalServerError, "internal", "内部错误") return } writeJSON(w, http.StatusOK, ss) } // publicNoteItem 公开列表项(元信息,不含全文)。 type publicNoteItem struct { Slug string `json:"slug"` Title string `json:"title"` Summary string `json:"summary"` Tags []string `json:"tags"` Pinned bool `json:"pinned"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } // handlePublicNotes GET /api/notes:公开笔记列表(可见性过滤在查询层,§9.1-T10)。 func (s *Server) handlePublicNotes(w http.ResponseWriter, r *http.Request) { page, pageSize, ok := s.parsePagination(r) if !ok { writeError(w, http.StatusBadRequest, "bad_request", "分页参数越界(page ∈ [1,10000],page_size ∈ [1,100])") return } tag := r.URL.Query().Get("tag") notes, total, err := s.st.ListPublicNotes(page, pageSize, tag) if err != nil { s.log.Error("查询公开列表失败", "err", err) writeError(w, http.StatusInternalServerError, "internal", "内部错误") return } items := make([]publicNoteItem, 0, len(notes)) for _, n := range notes { items = append(items, publicNoteItem{ Slug: n.Slug, Title: n.Title, Summary: n.Summary, Tags: n.Tags, Pinned: n.Pinned, CreatedAt: n.CreatedAt, UpdatedAt: n.UpdatedAt, }) } writeJSON(w, http.StatusOK, map[string]any{ "items": items, "page": page, "page_size": pageSize, "total": total, }) } // handlePublicNote GET /api/notes/{slug}:public 或管理员会话可读;其余统一 404。 func (s *Server) handlePublicNote(w http.ResponseWriter, r *http.Request) { slug := r.PathValue("slug") note, err := s.st.GetNoteBySlug(slug) if err != nil { if errors.Is(err, store.ErrNotFound) { writeError(w, http.StatusNotFound, "not_found", "笔记不存在") return } writeError(w, http.StatusInternalServerError, "internal", "内部错误") return } if note.DeletedAt != nil { // 回收站内容仅经 /api/admin/trash 出口可见(§13) writeError(w, http.StatusNotFound, "not_found", "笔记不存在") return } if note.Status != "public" { if _, isAdmin := s.sessionFrom(r); !isAdmin { // 私有笔记对匿名统一 404(不泄露存在性);管理员可私有预览 writeError(w, http.StatusNotFound, "not_found", "笔记不存在") return } } prevSlug, prevTitle, _ := s.st.AdjacentPublicNote(note, "prev") nextSlug, nextTitle, _ := s.st.AdjacentPublicNote(note, "next") writeJSON(w, http.StatusOK, map[string]any{ "id": note.ID, "slug": note.Slug, "title": note.Title, "summary": note.Summary, "content": note.Content, "status": note.Status, "tags": note.Tags, "pinned": note.Pinned, "created_at": note.CreatedAt, "updated_at": note.UpdatedAt, "prev": siblingOrEmpty(prevSlug, prevTitle), "next": siblingOrEmpty(nextSlug, nextTitle), }) } func siblingOrEmpty(slug, title string) map[string]any { if slug == "" { return nil } return map[string]any{"slug": slug, "title": title} } // handleTags GET /api/tags:标签聚合(仅公开且未删除)。 func (s *Server) handleTags(w http.ResponseWriter, r *http.Request) { tags, err := s.st.PublicTags() if err != nil { writeError(w, http.StatusInternalServerError, "internal", "内部错误") return } if tags == nil { tags = []store.TagCount{} } writeJSON(w, http.StatusOK, map[string]any{"tags": tags}) } // handleImage GET /api/images/{id}:并集可见性;未授权与不存在统一 404; // 缓存头按可见性分流(§7.4)。 func (s *Server) handleImage(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil || id <= 0 { writeError(w, http.StatusNotFound, "not_found", "图片不存在") return } img, err := s.st.GetImageMeta(id) if err != nil { writeError(w, http.StatusNotFound, "not_found", "图片不存在") return } public, err := s.st.ImageIsPublic(id) if err != nil { writeError(w, http.StatusInternalServerError, "internal", "内部错误") return } if !public { if _, isAdmin := s.sessionFrom(r); !isAdmin { writeError(w, http.StatusNotFound, "not_found", "图片不存在") return } // 非公开图:每次请求重新判定,禁止缓存 w.Header().Set("Cache-Control", "private, no-store") } else { w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") } w.Header().Set("Content-Type", img.MIME) w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("ETag", `"`+img.SHA256+`"`) if r.Header.Get("If-None-Match") == `"`+img.SHA256+`"` { w.WriteHeader(http.StatusNotModified) return } data, err := s.st.GetImageData(id) if err != nil { writeError(w, http.StatusNotFound, "not_found", "图片不存在") return } w.Header().Set("Content-Length", strconv.Itoa(len(data))) _, _ = w.Write(data) }