package handlers import ( "strings" "github.com/gin-gonic/gin" "git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response" "git.netcell-it.de/projekte/edgeguard-native/internal/services/audit" "git.netcell-it.de/projekte/edgeguard-native/internal/services/clusterjoin" "git.netcell-it.de/projekte/edgeguard-native/internal/services/setup" ) // SetupHandler exposes the first-run wizard endpoints. Both endpoints // are mounted before SetupGate so they remain reachable while the API // is in setup mode. type SetupHandler struct { Store *setup.Store Audit *audit.Repo NodeID string Version string } func NewSetupHandler(store *setup.Store) *SetupHandler { return &SetupHandler{Store: store} } // WithAudit injiziert Audit-Repo + Node-ID damit Mutationen (contact-emails) // in audit_log landen. Optional — wenn Audit nil bleibt, läuft die // Mutation, aber ohne Log-Eintrag. func (h *SetupHandler) WithAudit(a *audit.Repo, nodeID string) *SetupHandler { h.Audit = a h.NodeID = nodeID return h } // WithVersion macht die laufende Version für auto-register verfügbar. func (h *SetupHandler) WithVersion(v string) *SetupHandler { h.Version = v return h } 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) g.POST("/join-cluster", h.JoinCluster) } // RegisterAuthed mountet die Endpoints die nach abgeschlossenem Setup // den Admin-Modus brauchen — Aufrufer hat requireAuth schon dran. func (h *SetupHandler) RegisterAuthed(rg *gin.RouterGroup) { g := rg.Group("/setup") g.POST("/contact-emails", h.SetContactEmails) } // Status returns just the public bits of the setup state: whether // it's done and (if so) the configured admin_email + acme_email + // fqdn. Never exposes the password hash. func (h *SetupHandler) Status(c *gin.Context) { st, err := h.Store.Load() if err != nil { response.Internal(c, err) return } response.OK(c, gin.H{ "completed": st.Completed, "admin_email": st.AdminEmail, "acme_email": st.ACMEEmail, "fqdn": st.FQDN, }) } // SetContactEmails: Admin-only Update der zwei E-Mail-Felder. // Sessions bleiben aktiv (Cookie referenziert den alten Actor); auf // nächstem Login zählt der neue Wert. func (h *SetupHandler) SetContactEmails(c *gin.Context) { var req struct { AdminEmail string `json:"admin_email" binding:"required,email"` ACMEEmail string `json:"acme_email" binding:"required,email"` } if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err) return } if err := h.Store.SetContactEmails(req.AdminEmail, req.ACMEEmail); err != nil { response.BadRequest(c, err) return } st, err := h.Store.Load() if err != nil { response.Internal(c, err) return } if h.Audit != nil { _ = h.Audit.Log(c.Request.Context(), actorOf(c), "setup.contact_emails", st.AdminEmail, gin.H{ "admin_email": st.AdminEmail, "acme_email": st.ACMEEmail, }, h.NodeID) } response.OK(c, gin.H{ "admin_email": st.AdminEmail, "acme_email": st.ACMEEmail, }) } func (h *SetupHandler) Complete(c *gin.Context) { var req setup.Request if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err) return } st, err := h.Store.Complete(req) if err != nil { response.BadRequest(c, err) return } response.OK(c, gin.H{ "completed": st.Completed, "admin_email": st.AdminEmail, "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, }) } // JoinCluster performs the full cluster-join flow from the setup wizard: // fetches certs from the primary, writes them to disk, then marks setup // as completed. No CLI required. func (h *SetupHandler) JoinCluster(c *gin.Context) { var body struct { FQDN string `json:"fqdn" binding:"required"` ACMEEmail string `json:"acme_email" binding:"required,email"` PrimaryFQDN string `json:"primary_fqdn" binding:"required"` Token string `json:"token" binding:"required"` Insecure bool `json:"insecure"` } if err := c.ShouldBindJSON(&body); err != nil { response.BadRequest(c, err) return } fqdn := strings.ToLower(strings.TrimSpace(body.FQDN)) if err := clusterjoin.Join(clusterjoin.Request{ PrimaryFQDN: body.PrimaryFQDN, Token: strings.TrimSpace(body.Token), CommonName: fqdn, Insecure: body.Insecure, Force: true, // bootstrap self-signed cert must be replaced Version: h.Version, NodeID: h.NodeID, }); err != nil { response.BadRequest(c, err) return } st, err := h.Store.CompleteAsNode(setup.NodeRequest{ FQDN: fqdn, ACMEEmail: body.ACMEEmail, }) if err != nil { response.Internal(c, err) return } response.OK(c, gin.H{ "completed": st.Completed, "is_cluster_node": st.IsClusterNode, "fqdn": st.FQDN, }) }