package handlers import ( "encoding/json" "fmt" "html/template" "net/http" "time" "gitea.tecamino.local/paadi/licenseServer/internal/handlers/middleware" "gitea.tecamino.local/paadi/licenseServer/internal/models" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) // Handler holds shared dependencies for all HTTP handlers. type Handler struct { DB *models.DB Sessions *middleware.SessionStore Tmpl *template.Template } // --- Auth --- func (h *Handler) ShowLogin(w http.ResponseWriter, r *http.Request) { h.Tmpl.ExecuteTemplate(w, "login.html", nil) } func (h *Handler) PostLogin(w http.ResponseWriter, r *http.Request) { email := r.FormValue("email") pass := r.FormValue("password") id, hash, err := h.DB.GetUserByEmail(email) if err != nil || bcrypt.CompareHashAndPassword([]byte(hash), []byte(pass)) != nil { h.Tmpl.ExecuteTemplate(w, "login.html", map[string]string{"Error": "Invalid email or password"}) return } token := h.Sessions.Create(id) middleware.SetSession(w, token) http.Redirect(w, r, "/", http.StatusSeeOther) } func (h *Handler) PostLogout(w http.ResponseWriter, r *http.Request) { if cookie, err := r.Cookie("session"); err == nil { h.Sessions.Delete(cookie.Value) } middleware.ClearSession(w) http.Redirect(w, r, "/login", http.StatusSeeOther) } func (h *Handler) ShowSetup(w http.ResponseWriter, r *http.Request) { if h.DB.UserCount() > 0 { http.Redirect(w, r, "/", http.StatusSeeOther) return } h.Tmpl.ExecuteTemplate(w, "setup.html", nil) } func (h *Handler) PostSetup(w http.ResponseWriter, r *http.Request) { if h.DB.UserCount() > 0 { http.Redirect(w, r, "/", http.StatusSeeOther) return } email := r.FormValue("email") pass := r.FormValue("password") if email == "" || len(pass) < 8 { h.Tmpl.ExecuteTemplate(w, "setup.html", map[string]string{"Error": "Email required, password min 8 chars"}) return } hash, _ := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) h.DB.CreateUser(uuid.New().String(), email, string(hash)) http.Redirect(w, r, "/login", http.StatusSeeOther) } // --- Dashboard --- func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { licenses, err := h.DB.ListLicenses() if err != nil { http.Error(w, "DB error", 500) return } active, revoked := 0, 0 for _, l := range licenses { if l.Active { active++ } else { revoked++ } } h.Tmpl.ExecuteTemplate(w, "dashboard.html", map[string]any{ "Page": "dashboard", "Licenses": licenses, "Total": len(licenses), "ActiveCount": active, "RevokedCount": revoked, }) } // --- License management --- func (h *Handler) ShowNewLicense(w http.ResponseWriter, r *http.Request) { h.Tmpl.ExecuteTemplate(w, "new_license.html", map[string]string{ "Page": "new", "GeneratedKey": GenerateKey(), }) } func (h *Handler) PostNewLicense(w http.ResponseWriter, r *http.Request) { r.ParseForm() licType := r.FormValue("type") l := &models.License{ ID: uuid.New().String(), Key: r.FormValue("key"), CustomerName: r.FormValue("customer_name"), CustomerEmail: r.FormValue("customer_email"), ProductName: r.FormValue("product_name"), MachineID: r.FormValue("machine_id"), Type: licType, Active: true, CreatedAt: time.Now(), } if licType == "expiry" { t, err := time.Parse("2006-01-02", r.FormValue("expires_at")) if err == nil { l.ExpiresAt = &t } } else { n := 0 fmt.Sscanf(r.FormValue("max_uses"), "%d", &n) l.MaxUses = &n } if err := h.DB.CreateLicense(l); err != nil { fmt.Printf("CreateLicense error: %v\n", err) h.Tmpl.ExecuteTemplate(w, "new_license.html", map[string]any{ "Error": err.Error(), "GeneratedKey": l.Key, }) return } http.Redirect(w, r, "/", http.StatusSeeOther) } func (h *Handler) RevokeLicense(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") h.DB.RevokeLicense(id) w.Header().Set("HX-Refresh", "true") } func (h *Handler) ReinstateLicense(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") h.DB.ActivateLicense(id) w.Header().Set("HX-Refresh", "true") } // --- REST API for client apps --- type validateRequest struct { LicenseKey string `json:"license_key"` MachineID string `json:"machine_id"` Hostname string `json:"hostname"` OS string `json:"os"` } type validateResponse struct { Valid bool `json:"valid"` Message string `json:"message,omitempty"` Type string `json:"type,omitempty"` Expires *time.Time `json:"expires,omitempty"` } func (h *Handler) APIValidate(w http.ResponseWriter, r *http.Request) { var req validateRequest json.NewDecoder(r.Body).Decode(&req) w.Header().Set("Content-Type", "application/json") license, err := h.DB.GetLicenseByKey(req.LicenseKey) if err != nil || license == nil { json.NewEncoder(w).Encode(validateResponse{Valid: false, Message: "license not found"}) return } if !license.Active { json.NewEncoder(w).Encode(validateResponse{Valid: false, Message: "license revoked"}) return } // Machine binding check if license.MachineID != "" && license.MachineID != req.MachineID { json.NewEncoder(w).Encode(validateResponse{Valid: false, Message: "machine mismatch"}) return } // Expiry check if license.Type == "expiry" && license.ExpiresAt != nil { if time.Now().After(*license.ExpiresAt) { json.NewEncoder(w).Encode(validateResponse{Valid: false, Message: "license expired"}) return } } // Usage check if license.Type == "usage" && license.MaxUses != nil { if license.UsedCount >= *license.MaxUses { json.NewEncoder(w).Encode(validateResponse{Valid: false, Message: "usage limit reached"}) return } h.DB.IncrementUsage(license.ID) } // Record activation h.DB.RecordActivation(&models.Activation{ ID: uuid.New().String(), LicenseID: license.ID, MachineID: req.MachineID, Hostname: req.Hostname, OS: req.OS, ActivatedAt: time.Now(), }) json.NewEncoder(w).Encode(validateResponse{Valid: true, Type: license.Type, Expires: license.ExpiresAt}) }