评论阶段二:GitHub OAuth 登录 + 评论表/用户表 + 先审后显开关 + 禁言 + 后台评论管理页
- auth 包:读者会话(one_reader,与后台令牌互斥)+ GitHub OAuth 客户端;Verify 校验 HMAC 与 reader: 前缀 - 公开 API:auth 五端点、评论列表(顶层可见+自己待审、回复内嵌)、发表(登录/禁言/开关/500 字校验)、10 分钟编辑窗、软删墓碑 - 管理端:评论列表(待审/已通过/全部)、通过、软删、读者列表、禁言切换 - 前台:登录卡/禁言卡;后台:评论管理页 + 设置页审核开关
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -72,6 +73,24 @@ func (a *API) Routes() http.Handler {
|
||||
mux.HandleFunc("/api/admin/files", a.guard(a.files))
|
||||
mux.HandleFunc("/api/admin/files/", a.guard(a.fileByID))
|
||||
mux.HandleFunc("/api/admin/settings", a.guard(a.settings))
|
||||
mux.HandleFunc("/api/admin/comments", a.guard(a.adminComments))
|
||||
mux.HandleFunc("/api/admin/comments/", a.guard(a.adminCommentByID))
|
||||
mux.HandleFunc("/api/admin/readers", a.guard(a.adminReaders))
|
||||
mux.HandleFunc("/api/admin/readers/", a.guard(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 路径形如 /api/admin/readers/{id}/ban
|
||||
rest := strings.Trim(strings.TrimPrefix(r.URL.Path, "/api/admin/readers/"), "/")
|
||||
parts := strings.Split(rest, "/")
|
||||
if len(parts) != 2 || parts[1] != "ban" {
|
||||
httpx.NotFound(w)
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseInt(parts[0], 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
httpx.BadRequest(w, "bad reader id")
|
||||
return
|
||||
}
|
||||
a.adminReaderBan(w, r, id)
|
||||
}))
|
||||
return mux
|
||||
}
|
||||
|
||||
@@ -772,3 +791,98 @@ func (a *API) fileByID(w http.ResponseWriter, r *http.Request) {
|
||||
httpx.Error(w, http.StatusMethodNotAllowed, "GET/DELETE required")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- comments(评论审核与管理) ----------
|
||||
|
||||
func (a *API) adminComments(w http.ResponseWriter, r *http.Request) {
|
||||
status := httpx.QueryString(r, "status")
|
||||
page := httpx.QueryInt(r, "page", 1)
|
||||
size := httpx.QueryInt(r, "size", 20)
|
||||
fp, err := a.Store.ListCommentsAdmin(status, page, size)
|
||||
if err != nil {
|
||||
httpx.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.OK(w, fp)
|
||||
}
|
||||
|
||||
func (a *API) adminCommentByID(w http.ResponseWriter, r *http.Request) {
|
||||
rest := strings.Trim(strings.TrimPrefix(r.URL.Path, "/api/admin/comments/"), "/")
|
||||
id, err := parseInt(rest)
|
||||
if err != nil {
|
||||
httpx.BadRequest(w, "bad comment id")
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodPut:
|
||||
// 审核动作:{"status": "visible" | "pending"}
|
||||
var in struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := httpx.Decode(r, &in); err != nil {
|
||||
httpx.BadRequest(w, "invalid body")
|
||||
return
|
||||
}
|
||||
if in.Status != "visible" && in.Status != "pending" {
|
||||
httpx.BadRequest(w, "status 只支持 visible / pending")
|
||||
return
|
||||
}
|
||||
if err := a.Store.SetCommentStatus(id, in.Status); err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
httpx.NotFound(w)
|
||||
return
|
||||
}
|
||||
httpx.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.OK(w, map[string]any{"ok": true})
|
||||
case http.MethodDelete:
|
||||
// 后台删除同样走软删(墓碑保楼层)
|
||||
if err := a.Store.DeleteComment(id); err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
httpx.NotFound(w)
|
||||
return
|
||||
}
|
||||
httpx.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.OK(w, map[string]any{"ok": true})
|
||||
default:
|
||||
httpx.Error(w, http.StatusMethodNotAllowed, "PUT/DELETE required")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- readers(评论用户与禁言) ----------
|
||||
|
||||
func (a *API) adminReaders(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
readers, err := a.Store.ListReaders()
|
||||
if err != nil {
|
||||
httpx.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.OK(w, map[string]any{"readers": readers})
|
||||
default:
|
||||
httpx.Error(w, http.StatusMethodNotAllowed, "GET required")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *API) adminReaderBan(w http.ResponseWriter, r *http.Request, id int64) {
|
||||
var in struct {
|
||||
Banned bool `json:"banned"`
|
||||
}
|
||||
if err := httpx.Decode(r, &in); err != nil {
|
||||
httpx.BadRequest(w, "invalid body")
|
||||
return
|
||||
}
|
||||
if err := a.Store.SetReaderBanned(id, in.Banned); err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
httpx.NotFound(w)
|
||||
return
|
||||
}
|
||||
httpx.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.OK(w, map[string]any{"ok": true, "banned": in.Banned})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user