- 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>
284 lines
7.9 KiB
Go
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",
|
|
})
|
|
}
|