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
+100
View File
@@ -0,0 +1,100 @@
package api
import (
"encoding/json"
"errors"
"net/http"
"codeberg.org/SquidSE/ClusterCanvas/service/internal/auth"
"codeberg.org/SquidSE/ClusterCanvas/service/internal/settings"
)
type networkResponse struct {
Network settings.NetworkSettings `json:"network"`
}
type putNetworkRequest struct {
Network settings.NetworkSettings `json:"network"`
}
func networkGetHandler(configDir string) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
settingsPayload, err := loadSettingsOrDefault(configDir)
if err != nil {
writeJSON(writer, http.StatusInternalServerError, apiErrorResponse{Error: err.Error()})
return
}
writeJSON(writer, http.StatusOK, networkResponse{
Network: settings.EffectiveNetwork(settingsPayload.Network),
})
}
}
func networkPutHandler(configDir string) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
if err := authorizeNetworkWrite(request); err != nil {
writeJSON(writer, http.StatusForbidden, apiErrorResponse{Error: err.Error()})
return
}
var payload putNetworkRequest
if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
writeJSON(writer, http.StatusBadRequest, apiErrorResponse{Error: "invalid JSON body"})
return
}
if err := validateNetworkSettings(payload.Network); err != nil {
writeJSON(writer, http.StatusBadRequest, apiErrorResponse{Error: err.Error()})
return
}
network := settings.EffectiveNetwork(payload.Network)
clientIP := auth.ClientIP(request)
if wouldLockOut, reason := auth.WouldLockOut(clientIP, network); wouldLockOut {
message := "these settings would lock out your current IP"
if reason != "" {
message = message + ": " + reason
}
writeJSON(writer, http.StatusBadRequest, apiErrorResponse{Error: message})
return
}
settingsPayload, err := loadSettingsOrDefault(configDir)
if err != nil {
writeJSON(writer, http.StatusInternalServerError, apiErrorResponse{Error: err.Error()})
return
}
settingsPayload.Network = network
if err := settings.SaveSettings(configDir, settingsPayload); err != nil {
writeJSON(writer, http.StatusInternalServerError, apiErrorResponse{Error: err.Error()})
return
}
writeJSON(writer, http.StatusOK, networkResponse{Network: network})
}
}
func authorizeNetworkWrite(_ *http.Request) error {
// TODO: integrate authz middleware.
return nil
}
func validateNetworkSettings(network settings.NetworkSettings) error {
if err := auth.ValidateListenAddress(network.ListenAddress); err != nil {
return err
}
if err := auth.ValidateHostname(network.PublicHostname); err != nil {
return err
}
if err := auth.ValidateAccessRules(network.Rules); err != nil {
return err
}
switch network.AccessMode {
case settings.AccessModeOpen, settings.AccessModeWhitelist, settings.AccessModeBlacklist:
return nil
default:
return errors.New("network.access_mode must be open, whitelist, or blacklist")
}
}