Remove setup code from first-run initialization

This commit is contained in:
eezd committed 2026-10-21 14:19:11 +08:00
1 parent 8b54258df5
commit 8b54ccba18
10 files changed
+125 -88

No files matched your search

+3 -8
View File
@@ -38,8 +38,6 @@ type Server struct {
index *search.Index
// 串行化设置的"读取-合并-写入"
settingsMu sync.Mutex
// OnSetup 在首次初始化完成后调用(用于删除启动码文件)
OnSetup func()
// ThemesDir 指向数据目录中的主题目录;为空时只有内置主题。
ThemesDir string
PluginsDir string
@@ -228,21 +226,18 @@ func (s *Server) authState(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) setup(w http.ResponseWriter, r *http.Request) {
var body struct{ Code, Password string }
var body struct{ Password string }
if !readJSON(w, r, &body) {
return
}
switch err := s.auth.Setup(body.Code, body.Password); {
case errors.Is(err, auth.ErrBadSetupCode), errors.Is(err, auth.ErrWeakPassword):
switch err := s.auth.Setup(body.Password); {
case errors.Is(err, auth.ErrWeakPassword):
writeError(w, 400, err.Error())
case errors.Is(err, auth.ErrAlreadySetUp):
writeError(w, 409, err.Error())
case err != nil:
internalError(w, r, err)
default:
if s.OnSetup != nil {
s.OnSetup()
}
s.startSession(w, r, body.Password)
}
}
+97 -5
View File
@@ -5,6 +5,7 @@ import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/cookiejar"
@@ -148,11 +149,8 @@ func newFixture(t *testing.T) *fixture {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
am, err := auth.New(st)
if err != nil {
t.Fatal(err)
}
if err := am.Setup(am.SetupCode(), "password123"); err != nil {
am := auth.New(st)
if err := am.Setup("password123"); err != nil {
t.Fatal(err)
}
idx, err := search.Open(filepath.Join(dir, "index.db"), v)
@@ -645,3 +643,97 @@ func TestChangePassword(t *testing.T) {
t.Errorf("新密码登录,得到 %d", code)
}
}
func TestSetupWithoutCode(t *testing.T) {
st, err := store.Open(filepath.Join(t.TempDir(), "setup.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
server := New(nil, auth.New(st), st, nil, nil)
srv := httptest.NewServer(server.Handler(fstest.MapFS{}))
t.Cleanup(srv.Close)
f := &fixture{t: t, url: srv.URL}
owner := newClient()
before := decode[struct{ SetUp, Authenticated bool }](t, f.do(owner, "GET", "/api/v1/auth/state", "", nil))
if before.SetUp || before.Authenticated {
t.Fatal("新实例不应已初始化或登录")
}
if status := f.status(owner, "POST", "/api/v1/auth/setup", `{"password":"short"}`, nil); status != 400 {
t.Fatalf("弱密码应被拒绝: %d", status)
}
if status := f.status(owner, "POST", "/api/v1/auth/setup", `{"password":"password123"}`, nil); status != 200 {
t.Fatalf("无需启动码即可初始化: %d", status)
}
after := decode[struct{ SetUp, Authenticated bool }](t, f.do(owner, "GET", "/api/v1/auth/state", "", nil))
if !after.SetUp || !after.Authenticated {
t.Fatal("初始化后应自动登录")
}
if status := f.status(newClient(), "POST", "/api/v1/auth/setup", `{"password":"replacement"}`, nil); status != 409 {
t.Fatalf("重复初始化必须拒绝: %d", status)
}
// 新建 Manager 模拟重启,不能重新开放密码设置。
reopened := auth.New(st)
if err := reopened.Setup("replacement"); !errors.Is(err, auth.ErrAlreadySetUp) {
t.Fatalf("重启后不能覆盖密码: %v", err)
}
if _, err := reopened.Login("test", "password123"); err != nil {
t.Fatal("原密码应仍然有效", err)
}
}
func TestConcurrentSetupCannotOverwritePassword(t *testing.T) {
st, err := store.Open(filepath.Join(t.TempDir(), "setup.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
manager := auth.New(st)
server := New(nil, manager, st, nil, nil)
srv := httptest.NewServer(server.Handler(fstest.MapFS{}))
t.Cleanup(srv.Close)
passwords := []string{"password-one", "password-two"}
type outcome struct {
index, status int
err error
}
results := make(chan outcome, 2)
for index, password := range passwords {
go func() {
res, err := newClient().Post(srv.URL+"/api/v1/auth/setup", "application/json", strings.NewReader(`{"password":"`+password+`"}`))
if err != nil {
results <- outcome{index: index, err: err}
return
}
res.Body.Close()
results <- outcome{index: index, status: res.StatusCode}
}()
}
winner, conflicts := -1, 0
for range passwords {
result := <-results
if result.err != nil {
t.Fatal(result.err)
}
switch result.status {
case 200:
if winner >= 0 {
t.Fatal("初始化成功不止一次")
}
winner = result.index
case 409:
conflicts++
default:
t.Fatalf("初始化状态不符: %d", result.status)
}
}
if winner < 0 || conflicts != 1 {
t.Fatal("必须只有一个初始化请求成功")
}
if _, err := manager.Login("test", passwords[winner]); err != nil {
t.Fatal("成功请求的密码未保存", err)
}
if _, err := manager.Login("test", passwords[1-winner]); !errors.Is(err, auth.ErrBadCredentials) {
t.Fatal("失败请求不应修改密码", err)
}
}
+4 -7
View File
@@ -98,7 +98,7 @@
},
"/auth/setup": {
"post": {
"summary": "首次设置登录密码(需服务器数据目录中的启动码)",
"summary": "首次设置登录密码",
"tags": [
"登录"
],
@@ -112,15 +112,11 @@
"schema": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"password": {
"type": "string"
}
},
"required": [
"code",
"password"
]
}
@@ -139,7 +135,7 @@
}
},
"400": {
"description": "启动码错误或密码过短",
"description": "密码过短",
"content": {
"application/json": {
"schema": {
@@ -158,7 +154,8 @@
}
}
}
}
},
"description": "仅未初始化实例可设置密码;首次成功后自动登录,已初始化时返回 409。"
}
},
"/auth/login": {