feat(admin): 历史记录可在后台逐条删除/清空
此前后台**没有任何历史管理入口**,只有一条需要 admin token 的 DELETE /api/history/(清空全部),所以用户根本无法删除记录。 - history-server: 新增 DELETE /item?url=<url> 删除单条并重建分组; CORS 允许 Authorization 头 - admin-server: 新增 /admin/api/history(GET 列表 / DELETE 清空)与 /admin/api/history/item(DELETE 单条),服务端带 token 转发给 history-server —— 令牌只在容器内部使用,浏览器侧无需保存 - admin UI: 「历史访问」卡片,列出记录 + 逐条 ✕ + 清空全部(均二次确认) 标题为空或本身是 URL 时显示简洁域名
This commit is contained in:
@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@ -30,6 +31,8 @@ var (
|
||||
requireTokenForWrite = isTruthy(os.Getenv("NAVI_ADMIN_REQUIRE_TOKEN"))
|
||||
// 浏览器插件同步用的令牌(供安装引导页展示,方便用户拷贝)
|
||||
syncToken = os.Getenv("NAVI_SYNC_TOKEN")
|
||||
// history-server 地址(管理后台转发历史记录管理请求用)
|
||||
historyBaseURL = envOr("NAVI_HISTORY_URL", "http://127.0.0.1:3004")
|
||||
// 本地上传的图标目录(Dashy 通过 /item-icons/ 读取)
|
||||
iconDir = envOr("NAVI_ICON_DIR", filepath.Join(filepath.Dir(confFile), "item-icons"))
|
||||
// 壁纸目录(nginx 通过 /wallpapers/ 提供)
|
||||
@ -354,6 +357,71 @@ func jsonResp(w http.ResponseWriter, v any) {
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
// ── 历史记录管理 ──
|
||||
//
|
||||
// 门户对 /api/history/ 是放行的(不走登录),所以那条路径的 DELETE 必须自带 token。
|
||||
// 管理后台改走 /admin/api/history*:门户登录已是关卡,admin token 只在容器内部使用,
|
||||
// 不必让用户在浏览器里保存它。
|
||||
func historyProxy(method, path, token string) (*http.Response, error) {
|
||||
req, err := http.NewRequest(method, historyBaseURL+path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func relayHistory(w http.ResponseWriter, res *http.Response) {
|
||||
defer res.Body.Close()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(res.StatusCode)
|
||||
_, _ = io.Copy(w, res.Body)
|
||||
}
|
||||
|
||||
// handleHistoryList 列出历史记录(按访问次数降序)。
|
||||
func handleHistoryList(w http.ResponseWriter, r *http.Request) {
|
||||
res, err := historyProxy(http.MethodGet, "/", syncToken)
|
||||
if err != nil {
|
||||
jsonErr(w, 502, "history-server unreachable: "+err.Error())
|
||||
return
|
||||
}
|
||||
relayHistory(w, res)
|
||||
}
|
||||
|
||||
// handleHistoryClear 清空全部历史。
|
||||
func handleHistoryClear(w http.ResponseWriter, r *http.Request) {
|
||||
if !requireAdminToken(w, r) {
|
||||
return
|
||||
}
|
||||
res, err := historyProxy(http.MethodDelete, "/", adminToken)
|
||||
if err != nil {
|
||||
jsonErr(w, 502, "history-server unreachable: "+err.Error())
|
||||
return
|
||||
}
|
||||
relayHistory(w, res)
|
||||
}
|
||||
|
||||
// handleHistoryDeleteItem 删除单条历史。
|
||||
func handleHistoryDeleteItem(w http.ResponseWriter, r *http.Request) {
|
||||
if !requireAdminToken(w, r) {
|
||||
return
|
||||
}
|
||||
raw := r.URL.Query().Get("url")
|
||||
if raw == "" {
|
||||
jsonErr(w, 400, "url query parameter is required")
|
||||
return
|
||||
}
|
||||
res, err := historyProxy(http.MethodDelete, "/item?url="+url.QueryEscape(raw), adminToken)
|
||||
if err != nil {
|
||||
jsonErr(w, 502, "history-server unreachable: "+err.Error())
|
||||
return
|
||||
}
|
||||
relayHistory(w, res)
|
||||
}
|
||||
|
||||
func jsonErr(w http.ResponseWriter, status int, msg string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
@ -689,6 +757,24 @@ func main() {
|
||||
http.HandleFunc("/api/icon", cors(handleIconUpload))
|
||||
http.HandleFunc("/api/wallpaper", cors(handleWallpaper))
|
||||
http.HandleFunc("/api/sync-info", cors(handleSyncInfo))
|
||||
// 历史记录管理:列表 / 清空全部 / 删除单条
|
||||
http.HandleFunc("/api/history", cors(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
handleHistoryList(w, r)
|
||||
case http.MethodDelete:
|
||||
handleHistoryClear(w, r)
|
||||
default:
|
||||
jsonErr(w, 405, "method not allowed")
|
||||
}
|
||||
}))
|
||||
http.HandleFunc("/api/history/item", cors(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
jsonErr(w, 405, "method not allowed")
|
||||
return
|
||||
}
|
||||
handleHistoryDeleteItem(w, r)
|
||||
}))
|
||||
http.HandleFunc("/api/reload", cors(func(w http.ResponseWriter, r *http.Request) {
|
||||
jsonResp(w, map[string]string{"status": "saved"})
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user