feat(admin): add session-based portal login
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user