package handlers import ( "context" "encoding/json" "errors" "log/slog" "github.com/gin-gonic/gin" "git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response" "git.netcell-it.de/projekte/edgeguard-native/internal/license" "git.netcell-it.de/projekte/edgeguard-native/internal/services/audit" licsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/license" ) // LicenseHandler exposes: // GET /api/v1/license/status — current state from DB (cached) // POST /api/v1/license/verify — force live verify against server // PUT /api/v1/license/key — submit/replace license key + verify // DELETE /api/v1/license/key — clear key, fall back to trial type LicenseHandler struct { Repo *licsvc.Repo KeyStore *license.KeyStore Client *license.Client Audit *audit.Repo NodeID string } func NewLicenseHandler(repo *licsvc.Repo, ks *license.KeyStore, client *license.Client, a *audit.Repo, nodeID string) *LicenseHandler { return &LicenseHandler{Repo: repo, KeyStore: ks, Client: client, Audit: a, NodeID: nodeID} } func (h *LicenseHandler) Register(rg *gin.RouterGroup) { g := rg.Group("/license") g.GET("/status", h.Status) g.POST("/verify", h.Verify) g.PUT("/key", h.SetKey) g.DELETE("/key", h.ClearKey) } // Status returns the most recent verify result. If no row exists, // reports trial (license.Result with Type=trial — the client computes // trial expiry from the install-time). func (h *LicenseHandler) Status(c *gin.Context) { state, err := h.Repo.Get(c.Request.Context()) if err != nil { response.Internal(c, err) return } if state == nil { trial, terr := h.Client.Trial() if terr != nil || trial == nil { response.OK(c, gin.H{ "license_key": "", "status": "expired", "type": "trial", "valid": false, "reason": "trial expired", }) return } response.OK(c, gin.H{ "license_key": "", "status": trial.Status, "type": trial.Type, "expires_at": trial.ExpiresAt, "valid": trial.Valid, "reason": trial.Reason, }) return } response.OK(c, state) } // Verify forces a live verify against the license server using the // stored key. Useful for the "Re-verify"-button im UI. func (h *LicenseHandler) Verify(c *gin.Context) { key := h.KeyStore.Get() if key == "" { response.BadRequest(c, errors.New("no license key configured — use PUT /license/key first")) return } res, err := h.runVerifyAndPersist(c.Request.Context(), key) if err != nil { response.Err(c, 502, err) return } response.OK(c, res) } type setKeyReq struct { LicenseKey string `json:"license_key" binding:"required"` } // SetKey writes the new key to disk + immediately verifies it. If // verify fails, the key is still saved (operator can re-trigger // later when the network is back) but the response carries the error. func (h *LicenseHandler) SetKey(c *gin.Context) { var req setKeyReq if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err) return } if err := h.KeyStore.Save(req.LicenseKey); err != nil { response.Internal(c, err) return } _ = h.Audit.Log(c.Request.Context(), actorOf(c), "license.key.set", "license", nil, h.NodeID) res, err := h.runVerifyAndPersist(c.Request.Context(), req.LicenseKey) if err != nil { // Save succeeded, verify failed — return both so the UI can // surface the error without losing the saved-state info. response.OK(c, gin.H{ "saved": true, "verify_error": err.Error(), }) return } response.OK(c, res) } // ClearKey wipes the key file + license row. Box falls back to trial. func (h *LicenseHandler) ClearKey(c *gin.Context) { prev := h.KeyStore.Get() if err := h.KeyStore.Save(""); err != nil { response.Internal(c, err) return } if prev != "" { _ = h.Repo.Delete(c.Request.Context(), prev) } _ = h.Audit.Log(c.Request.Context(), actorOf(c), "license.key.clear", "license", nil, h.NodeID) response.NoContent(c) } // runVerifyAndPersist performs the live verify call and mirrors the // result into the licenses table. On error, marks last_error in DB // (status stays as before — grace). func (h *LicenseHandler) runVerifyAndPersist(ctx context.Context, key string) (*license.Result, error) { res, err := h.Client.Verify(key) if err != nil { _ = h.Repo.MarkError(ctx, key, err.Error()) slog.Warn("license: verify failed", "error", err) return nil, err } payload, _ := json.Marshal(res) status := "active" if !res.Valid { status = "expired" if res.Status == "revoked" { status = "invalid" } } if err := h.Repo.Upsert(ctx, key, status, res.ExpiresAt, h.NodeID, 0, payload, ""); err != nil { slog.Warn("license: db upsert failed", "error", err) } return res, nil }