diff --git a/admin-server/main.go b/admin-server/main.go index d26ada9..c0361b9 100644 --- a/admin-server/main.go +++ b/admin-server/main.go @@ -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"}) })) diff --git a/admin/index.html b/admin/index.html index 287ef34..f7e0be6 100644 --- a/admin/index.html +++ b/admin/index.html @@ -104,6 +104,14 @@ details summary{cursor:pointer;font-size:13px;font-weight:600;color:var(--tx2)}
+ +删除单条或清空全部浏览记录;删除后 Dashy 的「历史访问」分组会立即重建。
+