Add users and network settings tabs with admin protections.

List and delete accounts with last-admin and Administrators-group guards, and expose editable network settings via the configuration UI.
This commit is contained in:
2026-07-18 15:18:01 +02:00
parent f049f766d9
commit 2605c3b346
49 changed files with 5440 additions and 209 deletions
@@ -0,0 +1,95 @@
package api
import (
"bytes"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"codeberg.org/SquidSE/ClusterCanvas/service/internal/settings"
)
func TestSetupStatusIncomplete(t *testing.T) {
t.Parallel()
configDir := t.TempDir()
request := httptest.NewRequest(http.MethodGet, "/api/v1/setup/status", nil)
recorder := httptest.NewRecorder()
NewRouter(configDir).ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status %d", recorder.Code)
}
var payload setupStatusResponse
if err := json.NewDecoder(recorder.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
if payload.Completed {
t.Fatal("expected incomplete")
}
}
func TestSetupCompleteAndLogin(t *testing.T) {
configDir := t.TempDir()
keyBytes := make([]byte, 32)
for index := range keyBytes {
keyBytes[index] = byte(index + 3)
}
t.Setenv(settings.ConfigKeyEnvVar, base64.StdEncoding.EncodeToString(keyBytes))
router := NewRouter(configDir)
body := map[string]any{
"username": "Admin",
"password": "apple banana cherry date elderberry",
"totp_secret": "",
"totp_confirmed": false,
"network": map[string]any{
"listen_address": "0.0.0.0",
"public_hostname": "",
"access_mode": "open",
"rules": []string{},
},
"security": settings.DefaultSecuritySettings(),
}
encoded, err := json.Marshal(body)
if err != nil {
t.Fatal(err)
}
completeReq := httptest.NewRequest(http.MethodPost, "/api/v1/setup/complete", bytes.NewReader(encoded))
completeRec := httptest.NewRecorder()
router.ServeHTTP(completeRec, completeReq)
if completeRec.Code != http.StatusOK {
t.Fatalf("complete status %d body %s", completeRec.Code, completeRec.Body.String())
}
loginBody := []byte(`{"username":"Admin","password":"apple banana cherry date elderberry"}`)
loginReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(loginBody))
loginRec := httptest.NewRecorder()
router.ServeHTTP(loginRec, loginReq)
if loginRec.Code != http.StatusOK {
t.Fatalf("login status %d body %s", loginRec.Code, loginRec.Body.String())
}
var sessionCookie *http.Cookie
for _, cookie := range loginRec.Result().Cookies() {
if cookie.Name == "__Host-ClusterCanvas-Session" {
sessionCookie = cookie
break
}
}
if sessionCookie == nil {
t.Fatal("missing session cookie")
}
if !sessionCookie.Secure || !sessionCookie.HttpOnly || sessionCookie.Path != "/" {
t.Fatalf("cookie flags incorrect: %#v", sessionCookie)
}
meReq := httptest.NewRequest(http.MethodGet, "/api/v1/auth/me", nil)
meReq.AddCookie(sessionCookie)
meRec := httptest.NewRecorder()
router.ServeHTTP(meRec, meReq)
if meRec.Code != http.StatusOK {
t.Fatalf("me status %d body %s", meRec.Code, meRec.Body.String())
}
}