feat(setup): Cluster-Node-Modus im Setup-Wizard + Install-Script-Port-Fix
Setup-Wizard zeigt jetzt beim ersten Aufruf eine Modusauswahl: - "Neuinstallation" → bisheriger Flow (Admin-Account + FQDN + ACME) - "Cluster-Knoten beitreten" → nur FQDN + ACME, kein Admin-Account; nach dem Submit werden die cluster-join-Befehle direkt angezeigt Backend: NodeRequest + Store.CompleteAsNode() + POST /setup/complete-node State: is_cluster_node Flag; login via PG-Replikation vom Primary Install-Script: Setup-URL zeigt jetzt korrekt :3443/setup statt /setup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ func (h *SetupHandler) Register(rg *gin.RouterGroup) {
|
||||
g := rg.Group("/setup")
|
||||
g.GET("/status", h.Status)
|
||||
g.POST("/complete", h.Complete)
|
||||
g.POST("/complete-node", h.CompleteAsNode)
|
||||
}
|
||||
|
||||
// RegisterAuthed mountet die Endpoints die nach abgeschlossenem Setup
|
||||
@@ -111,3 +112,21 @@ func (h *SetupHandler) Complete(c *gin.Context) {
|
||||
"fqdn": st.FQDN,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *SetupHandler) CompleteAsNode(c *gin.Context) {
|
||||
var req setup.NodeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
st, err := h.Store.CompleteAsNode(req)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{
|
||||
"completed": st.Completed,
|
||||
"is_cluster_node": st.IsClusterNode,
|
||||
"fqdn": st.FQDN,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ type State struct {
|
||||
LicenseKey string `json:"license_key,omitempty"`
|
||||
Completed bool `json:"completed"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
// IsClusterNode marks this node as a secondary cluster member.
|
||||
// Admin credentials are absent — they arrive via PG replication.
|
||||
IsClusterNode bool `json:"is_cluster_node,omitempty"`
|
||||
|
||||
// MaintenanceMode (whole-box): wenn true rendert HAProxy auf
|
||||
// :443 nur einen 503-Block für ALLE Customer-Domains. Mgmt-UI
|
||||
@@ -75,6 +78,13 @@ type Request struct {
|
||||
LicenseKey string `json:"license_key,omitempty"`
|
||||
}
|
||||
|
||||
// NodeRequest is the JSON body POST /api/v1/setup/complete-node accepts.
|
||||
// No admin credentials — they are replicated from the primary via PG.
|
||||
type NodeRequest struct {
|
||||
FQDN string `json:"fqdn" binding:"required"`
|
||||
ACMEEmail string `json:"acme_email" binding:"required,email"`
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
Dir string
|
||||
}
|
||||
@@ -155,6 +165,37 @@ func (s *Store) Complete(req Request) (*State, error) {
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// CompleteAsNode configures this node as a secondary cluster member.
|
||||
// No admin credentials are created; login becomes available after PG
|
||||
// replication brings users from the primary.
|
||||
func (s *Store) CompleteAsNode(req NodeRequest) (*State, error) {
|
||||
if !looksLikeFQDN(req.FQDN) {
|
||||
return nil, fmt.Errorf("fqdn %q does not look like a fully-qualified hostname", req.FQDN)
|
||||
}
|
||||
if _, err := mail.ParseAddress(req.ACMEEmail); err != nil {
|
||||
return nil, fmt.Errorf("invalid acme_email: %w", err)
|
||||
}
|
||||
prev, err := s.Load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if prev.Completed {
|
||||
return nil, errors.New("setup already completed")
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
st := &State{
|
||||
FQDN: strings.TrimSpace(req.FQDN),
|
||||
ACMEEmail: strings.ToLower(strings.TrimSpace(req.ACMEEmail)),
|
||||
IsClusterNode: true,
|
||||
Completed: true,
|
||||
CompletedAt: &now,
|
||||
}
|
||||
if err := s.Save(st); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// VerifyAdminPassword does constant-time bcrypt comparison.
|
||||
func (st *State) VerifyAdminPassword(plaintext string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(st.AdminPasswordHash), []byte(plaintext)) == nil
|
||||
|
||||
Reference in New Issue
Block a user