feat(admin): add session-based portal login

This commit is contained in:
phamnazage-jpg
2026-05-28 11:01:29 +08:00
parent 03c4b5236f
commit de33ff3492
15 changed files with 833 additions and 75 deletions

View File

@@ -128,6 +128,122 @@ func TestAPIRejectsMissingAdminToken(t *testing.T) {
assertJSONContains(t, response.Body().Bytes(), "error.code", "unauthorized")
}
func TestAPIAdminSessionLoginSetsCookieAndAuthorizesSubsequentRequest(t *testing.T) {
handler := NewAPIHandlerWithAuth(AdminAuthConfig{
Token: "secret-token",
Username: "admin",
Password: "pass-123",
SessionTTL: 2 * time.Hour,
Now: func() time.Time {
return time.Unix(1_717_000_000, 0)
},
}, ActionSet{
ListPacks: func(context.Context) ([]PackInfo, error) {
return []PackInfo{{PackID: "openai-cn-pack", Version: "1.1.6"}}, nil
},
})
loginRequest := httptestRequest(t, http.MethodPost, "/api/admin/session/login", map[string]any{
"username": "admin",
"password": "pass-123",
}, "")
loginResponse := httptestRecorder(handler, loginRequest)
assertStatusCode(t, loginResponse, http.StatusOK)
assertJSONContains(t, loginResponse.Body().Bytes(), "authenticated", true)
assertJSONContains(t, loginResponse.Body().Bytes(), "username", "admin")
cookies := loginResponse.Result().Cookies()
if len(cookies) != 1 {
t.Fatalf("login cookies = %d, want 1", len(cookies))
}
if cookies[0].Name != adminSessionCookieName {
t.Fatalf("cookie name = %q, want %q", cookies[0].Name, adminSessionCookieName)
}
if !cookies[0].HttpOnly {
t.Fatal("session cookie HttpOnly = false, want true")
}
authorizedRequest := httptestRequest(t, http.MethodGet, "/api/packs", nil, "")
authorizedRequest.AddCookie(cookies[0])
authorizedResponse := httptestRecorder(handler, authorizedRequest)
assertStatusCode(t, authorizedResponse, http.StatusOK)
if !strings.Contains(authorizedResponse.Body().String(), `"pack_id":"openai-cn-pack"`) {
t.Fatalf("authorized response = %s, want pack_id openai-cn-pack", authorizedResponse.Body().String())
}
}
func TestAPIAdminSessionRejectsInvalidPassword(t *testing.T) {
handler := NewAPIHandlerWithAuth(AdminAuthConfig{
Token: "secret-token",
Username: "admin",
Password: "pass-123",
}, ActionSet{})
request := httptestRequest(t, http.MethodPost, "/api/admin/session/login", map[string]any{
"username": "admin",
"password": "wrong",
}, "")
response := httptestRecorder(handler, request)
assertStatusCode(t, response, http.StatusUnauthorized)
assertJSONContains(t, response.Body().Bytes(), "error.code", "unauthorized")
}
func TestAPIAdminSessionLogoutClearsCookie(t *testing.T) {
handler := NewAPIHandlerWithAuth(AdminAuthConfig{
Token: "secret-token",
Username: "admin",
Password: "pass-123",
}, ActionSet{})
request := httptestRequest(t, http.MethodPost, "/api/admin/session/logout", nil, "")
response := httptestRecorder(handler, request)
assertStatusCode(t, response, http.StatusNoContent)
cookies := response.Result().Cookies()
if len(cookies) != 1 {
t.Fatalf("logout cookies = %d, want 1", len(cookies))
}
if cookies[0].Name != adminSessionCookieName {
t.Fatalf("cookie name = %q, want %q", cookies[0].Name, adminSessionCookieName)
}
if cookies[0].MaxAge != -1 {
t.Fatalf("cookie MaxAge = %d, want -1", cookies[0].MaxAge)
}
}
func TestAPIAdminSessionMeReportsAuthenticationState(t *testing.T) {
now := time.Unix(1_717_000_000, 0)
handler := NewAPIHandlerWithAuth(AdminAuthConfig{
Token: "secret-token",
Username: "admin",
Password: "pass-123",
SessionTTL: time.Hour,
Now: func() time.Time {
return now
},
}, ActionSet{})
request := httptestRequest(t, http.MethodGet, "/api/admin/session", nil, "")
response := httptestRecorder(handler, request)
assertStatusCode(t, response, http.StatusOK)
assertJSONContains(t, response.Body().Bytes(), "authenticated", false)
assertJSONContains(t, response.Body().Bytes(), "login_enabled", true)
loginRequest := httptestRequest(t, http.MethodPost, "/api/admin/session/login", map[string]any{
"username": "admin",
"password": "pass-123",
}, "")
loginResponse := httptestRecorder(handler, loginRequest)
assertStatusCode(t, loginResponse, http.StatusOK)
meRequest := httptestRequest(t, http.MethodGet, "/api/admin/session", nil, "")
for _, cookie := range loginResponse.Result().Cookies() {
meRequest.AddCookie(cookie)
}
meResponse := httptestRecorder(handler, meRequest)
assertStatusCode(t, meResponse, http.StatusOK)
assertJSONContains(t, meResponse.Body().Bytes(), "authenticated", true)
assertJSONContains(t, meResponse.Body().Bytes(), "username", "admin")
}
func TestAPIInstallPackReturnsSummary(t *testing.T) {
handler := NewAPIHandler("secret-token", ActionSet{
InstallPack: func(context.Context, InstallPackRequest) (provision.PackInstallResult, error) {
@@ -545,10 +661,26 @@ type responseRecorder struct {
code int
}
func (r *responseRecorder) Header() http.Header { return r.header }
func (r *responseRecorder) Write(body []byte) (int, error) { return r.body.Write(body) }
func (r *responseRecorder) WriteHeader(statusCode int) { r.code = statusCode }
func (r *responseRecorder) Body() *bytes.Buffer { return &r.body }
func (r *responseRecorder) Header() http.Header { return r.header }
func (r *responseRecorder) Write(body []byte) (int, error) {
if r.code == 0 {
r.code = http.StatusOK
}
return r.body.Write(body)
}
func (r *responseRecorder) WriteHeader(statusCode int) { r.code = statusCode }
func (r *responseRecorder) Body() *bytes.Buffer { return &r.body }
func (r *responseRecorder) Result() *http.Response {
statusCode := r.code
if statusCode == 0 {
statusCode = http.StatusOK
}
return &http.Response{
StatusCode: statusCode,
Header: r.header.Clone(),
Body: io.NopCloser(bytes.NewReader(r.body.Bytes())),
}
}
func assertStatusCode(t *testing.T, recorder *responseRecorder, want int) {
t.Helper()