diff --git a/internal/httpapi/admin.go b/internal/httpapi/admin.go index 6f184e8..2d4fca2 100644 --- a/internal/httpapi/admin.go +++ b/internal/httpapi/admin.go @@ -396,6 +396,7 @@ type settingsDTO struct { SiteTitle *string `json:"site_title"` SiteDesc *string `json:"site_desc"` PageSize *int `json:"page_size"` + BeianNo *string `json:"beian_no"` } // handleAdminSettingsGet GET /api/admin/settings:白名单三键;永不序列化 admin_password_hash。 @@ -414,7 +415,7 @@ func (s *Server) handleAdminSettingsPut(w http.ResponseWriter, r *http.Request) dec.DisallowUnknownFields() var dto settingsDTO if err := dec.Decode(&dto); err != nil { - writeError(w, http.StatusBadRequest, "bad_request", "包含未知字段或类型不合法(白名单:site_title/site_desc/page_size)") + writeError(w, http.StatusBadRequest, "bad_request", "包含未知字段或类型不合法(白名单:site_title/site_desc/page_size/beian_no)") return } if dto.SiteTitle != nil { @@ -448,6 +449,17 @@ func (s *Server) handleAdminSettingsPut(w http.ResponseWriter, r *http.Request) return } } + if dto.BeianNo != nil { + v := strings.TrimSpace(*dto.BeianNo) + if len([]rune(v)) > 200 { + writeError(w, http.StatusBadRequest, "bad_request", "备案号 ≤200 字符") + return + } + if err := s.st.SetSetting(store.KeyBeianNo, v); err != nil { + writeError(w, http.StatusInternalServerError, "internal", "内部错误") + return + } + } // admin_password_hash 不可经此接口写入(§9.3):不在白名单结构体中,天然拒绝。 s.log.Info("admin_action", "op", "settings.update") ss, err := s.st.GetSiteSettings() diff --git a/internal/httpapi/api_test.go b/internal/httpapi/api_test.go index 2e14d1e..b584475 100644 --- a/internal/httpapi/api_test.go +++ b/internal/httpapi/api_test.go @@ -284,10 +284,31 @@ func TestSettingsWhitelist(t *testing.T) { } // PUT 合法键 resp, _ = e.do(admin, http.MethodPut, "/api/admin/settings", - []byte(`{"site_title":"新标题","site_desc":"描述","page_size":5}`), e.adminHeaders()) + []byte(`{"site_title":"新标题","site_desc":"描述","page_size":5,"beian_no":"京公网安备12345678901号"}`), e.adminHeaders()) if resp.StatusCode != 200 { t.Errorf("合法 PUT 应 200: %d", resp.StatusCode) } + // beian_no 经 /api/site 可读且生效 + _, body = e.get(e.client(), "/api/site") + if !strings.Contains(string(body), `"beian_no":"京公网安备12345678901号"`) { + t.Errorf("beian_no 设置应生效: %s", body) + } + // beian_no 可清空(空串 = 不展示) + resp, _ = e.do(admin, http.MethodPut, "/api/admin/settings", + []byte(`{"beian_no":""}`), e.adminHeaders()) + if resp.StatusCode != 200 { + t.Errorf("beian_no 清空应 200: %d", resp.StatusCode) + } + _, body = e.get(e.client(), "/api/site") + if !strings.Contains(string(body), `"beian_no":""`) { + t.Errorf("beian_no 应可清空: %s", body) + } + // beian_no 过长拒绝 + resp, _ = e.do(admin, http.MethodPut, "/api/admin/settings", + []byte(`{"beian_no":"`+strings.Repeat("备", 201)+`"}`), e.adminHeaders()) + if resp.StatusCode != http.StatusBadRequest { + t.Errorf("beian_no 超长应 400: %d", resp.StatusCode) + } // page_size 生效为公开列表默认 _, body = e.get(e.client(), "/api/notes") if !strings.Contains(string(body), `"page_size":5`) { diff --git a/internal/store/settings.go b/internal/store/settings.go index 23b1b76..eee6d1a 100644 --- a/internal/store/settings.go +++ b/internal/store/settings.go @@ -13,6 +13,7 @@ const ( KeySiteTitle = "site_title" KeySiteDesc = "site_desc" KeyPageSize = "page_size" + KeyBeianNo = "beian_no" ) // GetSetting 读取单个设置。 @@ -37,6 +38,7 @@ type SiteSettings struct { SiteTitle string `json:"site_title"` SiteDesc string `json:"site_desc"` PageSize int `json:"page_size"` + BeianNo string `json:"beian_no"` } // Defaults,未初始化时兜底。 @@ -44,11 +46,12 @@ const ( DefaultSiteTitle = "Pure Note" DefaultSiteDesc = "" DefaultPageSize = 10 + DefaultBeianNo = "" ) -// GetSiteSettings 读取站点设置(白名单三键,带默认值)。 +// GetSiteSettings 读取站点设置(白名单键,带默认值)。 func (s *Store) GetSiteSettings() (*SiteSettings, error) { - ss := &SiteSettings{SiteTitle: DefaultSiteTitle, SiteDesc: DefaultSiteDesc, PageSize: DefaultPageSize} + ss := &SiteSettings{SiteTitle: DefaultSiteTitle, SiteDesc: DefaultSiteDesc, PageSize: DefaultPageSize, BeianNo: DefaultBeianNo} if v, ok, err := s.GetSetting(KeySiteTitle); err != nil { return nil, err } else if ok && v != "" { @@ -67,5 +70,10 @@ func (s *Store) GetSiteSettings() (*SiteSettings, error) { ss.PageSize = n } } + if v, ok, err := s.GetSetting(KeyBeianNo); err != nil { + return nil, err + } else if ok { + ss.BeianNo = strings.TrimSpace(v) + } return ss, nil } diff --git a/web/public/national_emblem.png b/web/public/national_emblem.png new file mode 100644 index 0000000..9f76394 Binary files /dev/null and b/web/public/national_emblem.png differ diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx index 993a408..14e7e79 100644 --- a/web/src/components/Layout.tsx +++ b/web/src/components/Layout.tsx @@ -1,5 +1,5 @@ import { Link, NavLink, Outlet, useLocation, useNavigate } from 'react-router' -import { Moon, Sun, Monitor, ExternalLink, House, PenLine, Trash2, Settings, LogOut } from 'lucide-react' +import { Moon, Sun, Monitor, ExternalLink, House, PenLine, Tags, Trash2, Settings, LogOut } from 'lucide-react' import { useSiteTitle } from '../features/site' import { useTheme } from '../lib/theme' import { useAuth } from '../lib/auth' @@ -32,8 +32,12 @@ export function BlogLayout() {
显示在博客页脚并链接至公安部备案官网;留空则不显示。
+PURE NOTE · 极简笔记
-Pure Note 是一个纯文本驱动的极简笔记工具,这里是公开的笔记。
-