dal-license-server/internal/handler/dashboard_handler.go
djuka dc0114e4b7 Inicijalni commit: kompletna implementacija + dokumentacija + testovi
- Kompletna Go implementacija licencnog servera (19 Go fajlova)
- Klijentski API: activate, deactivate, validate
- Admin API: CRUD licence, stats, audit log
- Admin dashboard: htmx + Go templates
- RSA-2048 potpisivanje licencnih podataka
- Rate limiting i API key autentifikacija
- MySQL migracije i seed podaci (ESIR, ARV, LIGHT_TICKET)
- Unit testovi: keygen, crypto, model, middleware (24 testa)
- Dokumentacija: SPEC.md, ARCHITECTURE.md, SETUP.md, API.md, TESTING.md, README.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 07:42:25 +00:00

284 lines
7.9 KiB
Go

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",
})
}