package handler import ( "crypto/rand" "dal-license-server/internal/model" "dal-license-server/internal/repository" "dal-license-server/internal/service" "encoding/hex" "encoding/json" "html/template" "log" "net/http" "path/filepath" "strconv" "strings" "sync" "time" ) type DashboardHandler struct { licenses *service.LicenseService activation *service.ActivationService audit *repository.AuditRepo templates map[string]*template.Template sessions map[string]time.Time mu sync.RWMutex password string } func NewDashboardHandler(licenses *service.LicenseService, activation *service.ActivationService, audit *repository.AuditRepo, tmplDir, password string) *DashboardHandler { funcMap := template.FuncMap{ "formatDate": func(t time.Time) string { return t.Format("02.01.2006 15:04") }, "formatDateShort": func(t time.Time) string { return t.Format("02.01.2006") }, "json": func(v interface{}) string { b, _ := json.MarshalIndent(v, "", " ") return string(b) }, "jsonPretty": func(v json.RawMessage) string { var out interface{} json.Unmarshal(v, &out) b, _ := json.MarshalIndent(out, "", " ") return string(b) }, "add": func(a, b int) int { return a + b }, "maskKey": func(key string) string { if len(key) < 10 { return key } parts := strings.SplitN(key, "-", 2) prefix := parts[0] + "-" rest := parts[1] rParts := strings.Split(rest, "-") if len(rParts) >= 4 { return prefix + rParts[0] + "-****-****-" + rParts[3] } return key }, } h := &DashboardHandler{ licenses: licenses, activation: activation, audit: audit, templates: make(map[string]*template.Template), sessions: make(map[string]time.Time), password: password, } layoutFiles, _ := filepath.Glob(filepath.Join(tmplDir, "layout", "*.html")) partialFiles, _ := filepath.Glob(filepath.Join(tmplDir, "partials", "*.html")) baseFiles := append(layoutFiles, partialFiles...) pageFiles, _ := filepath.Glob(filepath.Join(tmplDir, "pages", "*.html")) for _, page := range pageFiles { name := filepath.Base(page) files := append(baseFiles, page) tmpl := template.Must(template.New(name).Funcs(funcMap).ParseFiles(files...)) h.templates[name] = tmpl } return h } func (h *DashboardHandler) render(w http.ResponseWriter, name string, data interface{}) { tmpl, ok := h.templates[name] if !ok { log.Printf("Template not found: %s", name) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := tmpl.ExecuteTemplate(w, name, data); err != nil { log.Printf("Template error (%s): %v", name, err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } func (h *DashboardHandler) isLoggedIn(r *http.Request) bool { c, err := r.Cookie("dash_session") if err != nil { return false } h.mu.RLock() defer h.mu.RUnlock() exp, ok := h.sessions[c.Value] if !ok || time.Now().After(exp) { return false } return true } func (h *DashboardHandler) RequireLogin(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !h.isLoggedIn(r) { http.Redirect(w, r, "/login", http.StatusSeeOther) return } next.ServeHTTP(w, r) }) } func (h *DashboardHandler) LoginPage(w http.ResponseWriter, r *http.Request) { h.render(w, "login.html", nil) } func (h *DashboardHandler) Login(w http.ResponseWriter, r *http.Request) { password := r.FormValue("password") if password != h.password { h.render(w, "login.html", map[string]interface{}{"Error": "Pogresna lozinka"}) return } b := make([]byte, 32) rand.Read(b) sid := hex.EncodeToString(b) h.mu.Lock() h.sessions[sid] = time.Now().Add(8 * time.Hour) h.mu.Unlock() http.SetCookie(w, &http.Cookie{ Name: "dash_session", Value: sid, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, }) http.Redirect(w, r, "/dashboard", http.StatusSeeOther) } func (h *DashboardHandler) Logout(w http.ResponseWriter, r *http.Request) { if c, err := r.Cookie("dash_session"); err == nil { h.mu.Lock() delete(h.sessions, c.Value) h.mu.Unlock() } http.SetCookie(w, &http.Cookie{Name: "dash_session", MaxAge: -1, Path: "/"}) http.Redirect(w, r, "/login", http.StatusSeeOther) } func (h *DashboardHandler) Dashboard(w http.ResponseWriter, r *http.Request) { stats, _ := h.licenses.GetStats() expiring, _ := h.licenses.ExpiringIn(7) recent, _ := h.audit.Recent(10) h.render(w, "dashboard.html", map[string]interface{}{ "Stats": stats, "Expiring": expiring, "Recent": recent, "ActivePage": "dashboard", }) } func (h *DashboardHandler) LicenseList(w http.ResponseWriter, r *http.Request) { product := r.URL.Query().Get("product") status := r.URL.Query().Get("status") search := r.URL.Query().Get("search") licenses, _ := h.licenses.List(product, status, search) products, _ := h.licenses.GetProducts() h.render(w, "licenses.html", map[string]interface{}{ "Licenses": licenses, "Products": products, "Product": product, "Status": status, "Search": search, "ActivePage": "licenses", }) } func (h *DashboardHandler) LicenseNew(w http.ResponseWriter, r *http.Request) { products, _ := h.licenses.GetProducts() h.render(w, "license-new.html", map[string]interface{}{ "Products": products, "ActivePage": "licenses", }) } func (h *DashboardHandler) LicenseCreate(w http.ResponseWriter, r *http.Request) { productID, _ := strconv.ParseInt(r.FormValue("product_id"), 10, 64) graceDays, _ := strconv.Atoi(r.FormValue("grace_days")) if graceDays == 0 { graceDays = 30 } limitsStr := r.FormValue("limits") var limits json.RawMessage if limitsStr != "" { limits = json.RawMessage(limitsStr) } featuresStr := r.FormValue("features") var features json.RawMessage if featuresStr != "" { features = json.RawMessage(featuresStr) } req := &model.CreateLicenseRequest{ ProductID: productID, LicenseType: r.FormValue("license_type"), CustomerName: r.FormValue("customer_name"), CustomerPIB: r.FormValue("customer_pib"), CustomerEmail: r.FormValue("customer_email"), Limits: limits, Features: features, GraceDays: graceDays, Notes: r.FormValue("notes"), } license, err := h.licenses.Create(req, clientIP(r)) if err != nil { products, _ := h.licenses.GetProducts() h.render(w, "license-new.html", map[string]interface{}{ "Products": products, "Error": err.Error(), "ActivePage": "licenses", }) return } http.Redirect(w, r, "/licenses/"+strconv.FormatInt(license.ID, 10), http.StatusSeeOther) } func (h *DashboardHandler) LicenseDetail(w http.ResponseWriter, r *http.Request) { id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64) license, err := h.licenses.GetByID(id) if err != nil { http.Error(w, "Licenca nije pronadjena", http.StatusNotFound) return } activations, _ := h.activation.ListByLicense(id) auditEntries, _ := h.audit.List(&id, 20) h.render(w, "license-detail.html", map[string]interface{}{ "License": license, "Activations": activations, "Audit": auditEntries, "ActivePage": "licenses", }) } func (h *DashboardHandler) LicenseRevoke(w http.ResponseWriter, r *http.Request) { id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64) reason := r.FormValue("reason") h.licenses.Revoke(id, reason, clientIP(r)) http.Redirect(w, r, "/licenses/"+strconv.FormatInt(id, 10), http.StatusSeeOther) } func (h *DashboardHandler) LicenseRelease(w http.ResponseWriter, r *http.Request) { id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64) h.activation.ForceRelease(id, clientIP(r)) http.Redirect(w, r, "/licenses/"+strconv.FormatInt(id, 10), http.StatusSeeOther) } func (h *DashboardHandler) AuditPage(w http.ResponseWriter, r *http.Request) { entries, _ := h.audit.Recent(100) h.render(w, "audit.html", map[string]interface{}{ "Entries": entries, "ActivePage": "audit", }) }