licence-server/internal/handler/dashboard_handler.go
djuka 04fd1678be Zateceno stanje sa produkcije: admin_users prijava + logo/favicon
Ovaj commit hvata radnu kopiju sa 151 kakva je zatecena 2026-07-25 (nije
pravljena u ovoj sesiji) - funkcionalnost prijave administratora preko
admin_users tabele (username/password_hash umesto jednog ADMIN_PASSWORD),
plus logo/favicon i go 1.24.0 + golang.org/x/crypto zavisnost. Binarni fajl
koji trenutno radi na produkciji je bas iz ovog stanja.

Zarobljeno kao osnova pre preimenovanja dal-license-server -> licence-server.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 05:47:42 +00:00

323 lines
9.1 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"
"golang.org/x/crypto/bcrypt"
)
type SessionData struct {
UserID int64
Username string
FullName string
Expiry time.Time
}
type DashboardHandler struct {
licenses *service.LicenseService
activation *service.ActivationService
audit *repository.AuditRepo
userRepo *repository.UserRepo
templates map[string]*template.Template
sessions map[string]*SessionData
mu sync.RWMutex
}
func NewDashboardHandler(licenses *service.LicenseService, activation *service.ActivationService, audit *repository.AuditRepo, userRepo *repository.UserRepo, tmplDir 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,
userRepo: userRepo,
templates: make(map[string]*template.Template),
sessions: make(map[string]*SessionData),
}
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) renderWithSession(w http.ResponseWriter, r *http.Request, name string, data map[string]interface{}) {
if sess := h.getSession(r); sess != nil {
data["CurrentUser"] = sess.FullName
if data["CurrentUser"] == "" {
data["CurrentUser"] = sess.Username
}
}
h.render(w, name, data)
}
func (h *DashboardHandler) getSession(r *http.Request) *SessionData {
c, err := r.Cookie("dash_session")
if err != nil {
return nil
}
h.mu.RLock()
defer h.mu.RUnlock()
sess, ok := h.sessions[c.Value]
if !ok || time.Now().After(sess.Expiry) {
return nil
}
return sess
}
func (h *DashboardHandler) RequireLogin(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if h.getSession(r) == nil {
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) {
username := r.FormValue("username")
password := r.FormValue("password")
user, err := h.userRepo.GetByUsername(username)
if err != nil || !user.Active {
h.render(w, "login.html", map[string]interface{}{"Error": "Pogresno korisnicko ime ili lozinka"})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
h.render(w, "login.html", map[string]interface{}{"Error": "Pogresno korisnicko ime ili lozinka"})
return
}
b := make([]byte, 32)
rand.Read(b)
sid := hex.EncodeToString(b)
h.mu.Lock()
h.sessions[sid] = &SessionData{
UserID: user.ID,
Username: user.Username,
FullName: user.FullName,
Expiry: time.Now().Add(8 * time.Hour),
}
h.mu.Unlock()
h.userRepo.UpdateLastLogin(user.ID)
h.audit.Log(nil, "LOGIN", clientIP(r), map[string]string{"username": user.Username})
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()
sess := h.sessions[c.Value]
delete(h.sessions, c.Value)
h.mu.Unlock()
if sess != nil {
h.audit.Log(nil, "LOGOUT", clientIP(r), map[string]string{"username": sess.Username})
}
}
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.renderWithSession(w, r, "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.renderWithSession(w, r, "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.renderWithSession(w, r, "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.renderWithSession(w, r, "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.renderWithSession(w, r, "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.renderWithSession(w, r, "audit.html", map[string]interface{}{
"Entries": entries,
"ActivePage": "audit",
})
}