fix: harden auth, config access, and backups

This commit is contained in:
sagitchu
2026-05-13 23:53:06 +08:00
parent ec9fb77eb5
commit 465815cf34
28 changed files with 1376 additions and 97 deletions
@@ -1,6 +1,7 @@
package contract_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
@@ -9,6 +10,7 @@ import (
"go-backend/internal/auth"
"go-backend/internal/http/middleware"
"go-backend/internal/http/response"
"go-backend/internal/security"
)
func TestJWTMiddlewareContracts(t *testing.T) {
@@ -18,7 +20,9 @@ func TestJWTMiddlewareContracts(t *testing.T) {
response.WriteJSON(w, response.OK("pass"))
})
wrapped := middleware.JWT(middleware.AuthOptions{JWTSecret: secret})(next)
wrapped := middleware.JWT(middleware.AuthOptions{JWTSecret: secret, GetUserAuthState: func(userID int64) (*auth.UserAuthState, error) {
return &auth.UserAuthState{ID: userID, RoleID: 0, Status: 1, PasswordChangedAt: 0}, nil
}})(next)
t.Run("login path is excluded", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/v1/user/login", nil)
@@ -59,6 +63,9 @@ func TestJWTMiddlewareContracts(t *testing.T) {
if err != nil {
t.Fatalf("generate token: %v", err)
}
wrapped := middleware.JWT(middleware.AuthOptions{JWTSecret: secret, GetUserAuthState: func(userID int64) (*auth.UserAuthState, error) {
return &auth.UserAuthState{ID: userID, RoleID: 1, Status: 1, PasswordChangedAt: 0}, nil
}})(next)
req := httptest.NewRequest(http.MethodPost, "/api/v1/config/update", nil)
req.Header.Set("Authorization", token)
res := httptest.NewRecorder()
@@ -67,6 +74,83 @@ func TestJWTMiddlewareContracts(t *testing.T) {
})
}
func TestLoginTokenValidatesThroughRouter(t *testing.T) {
router, r := setupContractRouter(t, "contract-jwt-secret")
seedLegacyUser(t, r, 9110, "router-login-user", "router-login-pass")
body := bytes.NewBufferString(`{"username":"router-login-user","password":"router-login-pass","captchaId":""}`)
req := httptest.NewRequest(http.MethodPost, "/api/v1/user/login", body)
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
var out response.R
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
t.Fatalf("decode login response: %v", err)
}
if out.Code != 0 {
t.Fatalf("expected login code 0, got %d (%s)", out.Code, out.Msg)
}
data, ok := out.Data.(map[string]interface{})
if !ok {
t.Fatalf("expected login data map, got %T", out.Data)
}
token, _ := data["token"].(string)
if token == "" {
t.Fatal("expected login token")
}
checkReq := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/user/tunnel", nil)
checkReq.Header.Set("Authorization", token)
checkResp := httptest.NewRecorder()
router.ServeHTTP(checkResp, checkReq)
assertCode(t, checkResp, 0)
}
func TestLegacyPasswordMigratesOnLogin(t *testing.T) {
router, r := setupContractRouter(t, "contract-jwt-secret")
legacyChangedAt := seedLegacyUser(t, r, 9101, "legacy-login-user", "legacy-login-pass")
body := bytes.NewBufferString(`{"username":"legacy-login-user","password":"legacy-login-pass","captchaId":""}`)
req := httptest.NewRequest(http.MethodPost, "/api/v1/user/login", body)
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assertCode(t, resp, 0)
assertUserPasswordIsBcrypt(t, r, "legacy-login-user", "legacy-login-pass")
if changedAt := mustQueryPasswordChangedAtByUsername(t, r, "legacy-login-user"); changedAt <= legacyChangedAt {
t.Fatalf("expected password_changed_at to advance on login migration, got %d <= %d", changedAt, legacyChangedAt)
}
}
func TestDisabledLegacyPasswordIsRejectedWithoutMigration(t *testing.T) {
router, r := setupContractRouter(t, "contract-jwt-secret")
legacyChangedAt := seedLegacyUserWithStatus(t, r, 9105, "disabled-legacy-user", "disabled-legacy-pass", 0)
body := bytes.NewBufferString(`{"username":"disabled-legacy-user","password":"disabled-legacy-pass","captchaId":""}`)
req := httptest.NewRequest(http.MethodPost, "/api/v1/user/login", body)
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assertCodeMsg(t, resp, -1, "账号被停用")
user, err := r.GetUserByUsername("disabled-legacy-user")
if err != nil {
t.Fatalf("get user: %v", err)
}
if user == nil {
t.Fatal("expected disabled user to exist")
}
if ok, migrated := security.VerifyPassword(user.Pwd, "disabled-legacy-pass"); !ok || !migrated {
t.Fatalf("expected disabled user to remain legacy MD5, got (%v,%v) with hash %q", ok, migrated, user.Pwd)
}
if changedAt := mustQueryPasswordChangedAtByUsername(t, r, "disabled-legacy-user"); changedAt != legacyChangedAt {
t.Fatalf("expected password_changed_at to remain unchanged, got %d want %d", changedAt, legacyChangedAt)
}
}
func assertCode(t *testing.T, rec *httptest.ResponseRecorder, expected int) {
t.Helper()
var out response.R