diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go
index 39a8f7e..87437a6 100644
--- a/cmd/ntech/main.go
+++ b/cmd/ntech/main.go
@@ -190,40 +190,40 @@ func main() {
r.Get("/magacin/kategorije", h.Kategorije)
r.Post("/magacin/kategorije/dodaj", h.DodajKategoriju)
r.Get("/magacin/kategorije/obrisi/{id}", h.ObrisiKategoriju)
- r.Get("/nabavke", h.Nabavke)
- r.Get("/nabavke/nova", h.NovaNabavka)
+ r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "nabavka.pregled")).Get("/nabavke", h.Nabavke)
+ r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "nabavka.pregled")).Get("/nabavke/nova", h.NovaNabavka)
r.Post("/nabavke/nova", h.SacuvajNabavku)
- r.Get("/nabavke/{id}", h.DetaljiNabavke)
+ r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "nabavka.pregled")).Get("/nabavke/{id}", h.DetaljiNabavke)
r.Post("/nabavke/obrisi/{id}", h.ObrisiNabavku)
- r.Get("/dobavljaci", h.Dobavljaci)
+ r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "dobavljac.pregled")).Get("/dobavljaci", h.Dobavljaci)
r.Get("/dobavljaci/novi", h.NoviDobavljac)
r.Post("/dobavljaci/novi", h.SacuvajDobavljaca)
r.Get("/dobavljaci/izmeni/{id}", h.IzmeniDobavljaca)
r.Post("/dobavljaci/izmeni/{id}", h.SacuvajIzmeneDobavljaca)
r.Post("/dobavljaci/obrisi/{id}", h.ObrisiDobavljaca)
- r.Get("/klijenti", h.Klijenti)
+ r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "klijent.pregled")).Get("/klijenti", h.Klijenti)
r.Get("/klijenti/novi", h.NoviKlijent)
r.Post("/klijenti/novi", h.SacuvajKlijenta)
r.Get("/klijenti/izmeni/{id}", h.IzmeniKlijenta)
r.Post("/klijenti/izmeni/{id}", h.SacuvajIzmenuKlijenta)
r.Post("/klijenti/obrisi/{id}", h.ObrisiKlijenta)
- r.Get("/servis", h.Servis)
+ r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis", h.Servis)
r.Get("/servis/novi", h.NoviNalog)
r.Post("/servis/novi", h.SacuvajNalog)
r.Get("/servis/izmeni/{id}", h.IzmeniNalog)
r.Post("/servis/izmeni/{id}", h.SacuvajIzmenaNaloga)
r.Post("/servis/obrisi/{id}", h.ObrisiNalog)
- r.Get("/servis/{id}", h.DetaljiNaloga)
+ r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}", h.DetaljiNaloga)
r.Post("/servis/{id}/delovi", h.DodajDeloNalogu)
r.Post("/servis/{id}/delovi/{deo_id}/obrisi", h.ObrisiDeloNaloga)
r.Get("/izvestaji", h.Izvestaji)
- r.Get("/prodaja", h.Prodaja)
+ r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "prodaja.pregled")).Get("/prodaja", h.Prodaja)
r.Get("/prodaja/nova", h.NovaProdaja)
r.Post("/prodaja/nova", h.SacuvajProdaju)
r.Post("/prodaja/obrisi/{id}", h.ObrisiProdaju)
r.Post("/prodaja/storno/{id}", h.StornoProdaje)
- r.Get("/prodaja/{id}/stampa", h.StampaProdaje)
- r.Get("/prodaja/{id}", h.DetaljiProdaje)
+ r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "prodaja.pregled")).Get("/prodaja/{id}/stampa", h.StampaProdaje)
+ r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "prodaja.pregled")).Get("/prodaja/{id}", h.DetaljiProdaje)
// podsetnici
r.Get("/podsetnici", h.Podsetnici)
diff --git a/internal/handler/dobavljac.go b/internal/handler/dobavljac.go
index d201773..33df8da 100644
--- a/internal/handler/dobavljac.go
+++ b/internal/handler/dobavljac.go
@@ -30,11 +30,6 @@ type PodaciFormeDobavljaca struct {
// Dobavljaci renderuje listu svih dobavljača
func (h *Handler) Dobavljaci(w http.ResponseWriter, r *http.Request) {
- k := middleware.KorisnikIzKonteksta(r.Context())
- if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "dobavljac.pregled") {
- http.Error(w, "Nemate dozvolu za pregled dobavljača.", http.StatusForbidden)
- return
- }
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
diff --git a/internal/handler/kes.go b/internal/handler/kes.go
index cf64443..d7d758e 100644
--- a/internal/handler/kes.go
+++ b/internal/handler/kes.go
@@ -36,6 +36,26 @@ var standaloneIme = []string{
"prijava", "setup", "totp_provera", "prodaja_stampa",
}
+// sablonskeFunkcije su pomoćne funkcije dostupne u svim šablonima.
+// dict gradi mapu iz parova ključ/vrednost — koristi se da se jednom partialu
+// prosledi više vrednosti (npr. {{template "x" (dict "ID" .ID "Lista" $.Lista)}}).
+var sablonskeFunkcije = template.FuncMap{
+ "dict": func(parovi ...any) (map[string]any, error) {
+ if len(parovi)%2 != 0 {
+ return nil, fmt.Errorf("dict: neparan broj argumenata")
+ }
+ m := make(map[string]any, len(parovi)/2)
+ for i := 0; i < len(parovi); i += 2 {
+ kljuc, ok := parovi[i].(string)
+ if !ok {
+ return nil, fmt.Errorf("dict: ključ mora biti string")
+ }
+ m[kljuc] = parovi[i+1]
+ }
+ return m, nil
+ },
+}
+
// KreirajKes parsuje sve šablone iz fsys i vraća ih keširane u mapi
func KreirajKes(fsys fs.FS) (map[string]*template.Template, error) {
kes := make(map[string]*template.Template)
@@ -44,7 +64,7 @@ func KreirajKes(fsys fs.FS) (map[string]*template.Template, error) {
fajlovi := make([]string, len(bazniSabloni), len(bazniSabloni)+1)
copy(fajlovi, bazniSabloni)
fajlovi = append(fajlovi, "web/templates/stranice/"+ime+".html")
- t, err := template.ParseFS(fsys, fajlovi...)
+ t, err := template.New(ime).Funcs(sablonskeFunkcije).ParseFS(fsys, fajlovi...)
if err != nil {
return nil, fmt.Errorf("kes: %s: %w", ime, err)
}
@@ -80,7 +100,7 @@ func (h *Handler) renderujTemplate(w http.ResponseWriter, ime string, podaci any
copy(fajlovi, bazniSabloni)
fajlovi = append(fajlovi, "web/templates/stranice/"+ime+".html")
var err error
- if tmpl, err = template.ParseFS(h.TemplatesFS, fajlovi...); err != nil {
+ if tmpl, err = template.New(ime).Funcs(sablonskeFunkcije).ParseFS(h.TemplatesFS, fajlovi...); err != nil {
log.Printf("greška pri parsiranju šablona %s: %v", ime, err)
http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError)
return
diff --git a/internal/handler/klijent.go b/internal/handler/klijent.go
index 808aec9..ec2d88b 100644
--- a/internal/handler/klijent.go
+++ b/internal/handler/klijent.go
@@ -31,11 +31,6 @@ type PodaciFormeKlijenta struct {
// Klijenti renderuje listu svih klijenata sa opcionom pretragom
func (h *Handler) Klijenti(w http.ResponseWriter, r *http.Request) {
- k := middleware.KorisnikIzKonteksta(r.Context())
- if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "klijent.pregled") {
- http.Error(w, "Nemate dozvolu za pregled klijenata.", http.StatusForbidden)
- return
- }
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
diff --git a/internal/handler/nabavka.go b/internal/handler/nabavka.go
index 6b58e57..efa36e0 100644
--- a/internal/handler/nabavka.go
+++ b/internal/handler/nabavka.go
@@ -57,12 +57,6 @@ func artikalUJSON(artikli []model.ArtikalSaKategorijom) template.JS {
// Nabavke renderuje listu svih nabavki
func (h *Handler) Nabavke(w http.ResponseWriter, r *http.Request) {
- k := middleware.KorisnikIzKonteksta(r.Context())
- if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "nabavka.pregled") {
- http.Error(w, "Nemate dozvolu za pregled nabavki.", http.StatusForbidden)
- return
- }
-
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
@@ -90,12 +84,6 @@ func (h *Handler) Nabavke(w http.ResponseWriter, r *http.Request) {
// NovaNabavka prikazuje formu za unos nove nabavke
func (h *Handler) NovaNabavka(w http.ResponseWriter, r *http.Request) {
- k := middleware.KorisnikIzKonteksta(r.Context())
- if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "nabavka.pregled") {
- http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
- return
- }
-
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
@@ -175,12 +163,6 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
// DetaljiNabavke prikazuje pregled jedne nabavke sa svim stavkama
func (h *Handler) DetaljiNabavke(w http.ResponseWriter, r *http.Request) {
- k := middleware.KorisnikIzKonteksta(r.Context())
- if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "nabavka.pregled") {
- http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
- return
- }
-
id, err := parseID(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "Neispravan ID nabavke", http.StatusBadRequest)
diff --git a/internal/handler/podesavanja.go b/internal/handler/podesavanja.go
index 0e2ee1d..e5a3b62 100644
--- a/internal/handler/podesavanja.go
+++ b/internal/handler/podesavanja.go
@@ -248,22 +248,38 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
}
}
- // backup podešavanja — čuvamo samo ako su validni pozitivni brojevi u razumnom opsegu
- if v := r.FormValue("backup_interval_sati"); v != "" {
- if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 720 {
- _ = ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "backup_interval_sati", strconv.Itoa(n))
- }
- }
- if v := r.FormValue("backup_broj_kopija"); v != "" {
- if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 100 {
- _ = ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "backup_broj_kopija", strconv.Itoa(n))
- }
- }
-
sledeci := r.FormValue("_next")
if sledeci == "" || !strings.HasPrefix(sledeci, "/") {
sledeci = "/podesavanja"
}
+
+ // backup podešavanja — pri neispravnom unosu javljamo jasnu grešku
+ // umesto da ga tiho preskočimo a korisniku prikažemo "sačuvano"
+ if v := r.FormValue("backup_interval_sati"); v != "" {
+ n, err := strconv.Atoi(v)
+ if err != nil || n < 1 || n > 720 {
+ middleware.SetFlash(w, r, h.DB, "greska", "Razmak između backupa mora biti broj između 1 i 720 sati.")
+ http.Redirect(w, r, sledeci, http.StatusSeeOther)
+ return
+ }
+ if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "backup_interval_sati", strconv.Itoa(n)); err != nil {
+ http.Error(w, "Greška pri čuvanju podešavanja", http.StatusInternalServerError)
+ return
+ }
+ }
+ if v := r.FormValue("backup_broj_kopija"); v != "" {
+ n, err := strconv.Atoi(v)
+ if err != nil || n < 1 || n > 100 {
+ middleware.SetFlash(w, r, h.DB, "greska", "Broj kopija mora biti broj između 1 i 100.")
+ http.Redirect(w, r, sledeci, http.StatusSeeOther)
+ return
+ }
+ if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "backup_broj_kopija", strconv.Itoa(n)); err != nil {
+ http.Error(w, "Greška pri čuvanju podešavanja", http.StatusInternalServerError)
+ return
+ }
+ }
+
http.Redirect(w, r, sledeci+"?sacuvano=1", http.StatusSeeOther)
}
diff --git a/internal/handler/prodaja.go b/internal/handler/prodaja.go
index f9f50f4..91a2a20 100644
--- a/internal/handler/prodaja.go
+++ b/internal/handler/prodaja.go
@@ -76,11 +76,6 @@ func artikalUJSONSaCenom(artikli []model.ArtikalSaKategorijom) template.JS {
// Prodaja renderuje listu svih prodajnih naloga
func (h *Handler) Prodaja(w http.ResponseWriter, r *http.Request) {
- k := middleware.KorisnikIzKonteksta(r.Context())
- if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "prodaja.pregled") {
- http.Error(w, "Nemate dozvolu za pregled prodaje.", http.StatusForbidden)
- return
- }
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
@@ -208,11 +203,6 @@ func (h *Handler) SacuvajProdaju(w http.ResponseWriter, r *http.Request) {
// DetaljiProdaje prikazuje pregled jednog prodajnog naloga sa svim stavkama
func (h *Handler) DetaljiProdaje(w http.ResponseWriter, r *http.Request) {
- k := middleware.KorisnikIzKonteksta(r.Context())
- if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "prodaja.pregled") {
- http.Error(w, "Nemate dozvolu za pregled prodaje.", http.StatusForbidden)
- return
- }
id, err := parseID(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
@@ -265,11 +255,6 @@ func (h *Handler) DetaljiProdaje(w http.ResponseWriter, r *http.Request) {
// StampaProdaje renderuje print-friendly stranicu za dati prodajni nalog
func (h *Handler) StampaProdaje(w http.ResponseWriter, r *http.Request) {
- k := middleware.KorisnikIzKonteksta(r.Context())
- if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "prodaja.pregled") {
- http.Error(w, "Nemate dozvolu za pregled prodaje.", http.StatusForbidden)
- return
- }
id, err := parseID(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
diff --git a/internal/handler/servis.go b/internal/handler/servis.go
index 7c1a1be..efb888e 100644
--- a/internal/handler/servis.go
+++ b/internal/handler/servis.go
@@ -50,11 +50,6 @@ type PodaciDetaljiNaloga struct {
// Servis renderuje listu servisnih naloga sa opcionom pretragom i filterom statusa
func (h *Handler) Servis(w http.ResponseWriter, r *http.Request) {
- k := middleware.KorisnikIzKonteksta(r.Context())
- if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "servis.pregled") {
- http.Error(w, "Nemate dozvolu za pregled servisnih naloga.", http.StatusForbidden)
- return
- }
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
@@ -312,11 +307,6 @@ func (h *Handler) ObrisiNalog(w http.ResponseWriter, r *http.Request) {
// DetaljiNaloga prikazuje sve podatke jednog servisnog naloga sa ugrađenim delovima
func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
- k := middleware.KorisnikIzKonteksta(r.Context())
- if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "servis.pregled") {
- http.Error(w, "Nemate dozvolu za pregled servisnih naloga.", http.StatusForbidden)
- return
- }
id, err := parseID(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go
index 5b46b6e..0675f97 100644
--- a/internal/middleware/auth.go
+++ b/internal/middleware/auth.go
@@ -109,6 +109,24 @@ func RequireAdmin(next http.Handler) http.Handler {
})
}
+// RequireDozvola je middleware koji propušta korisnika samo ako njegova uloga
+// ima traženu akciju. Provera je DB-backed (prosleđuje se DozvoleRepo.ImaDozvolu),
+// tako da poštuje izmene matrice dozvola. Koristi se na nivou rute za "pregled"
+// stranica, umesto ponavljanja iste provere u svakom handleru.
+func RequireDozvola(proveri func(ctx context.Context, uloga, akcija string) bool, akcija string) func(http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ k := KorisnikIzKonteksta(r.Context())
+ if k == nil || !proveri(r.Context(), k.Uloga, akcija) {
+ postaviFlashGresku(w, "Nemate dozvolu za ovu stranicu.")
+ http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
+ return
+ }
+ next.ServeHTTP(w, r)
+ })
+ }
+}
+
// postaviFlashGresku upisuje jednokratnu poruku o grešci u kolačić
func postaviFlashGresku(w http.ResponseWriter, poruka string) {
http.SetCookie(w, &http.Cookie{
diff --git a/web/templates/stranice/magacin.html b/web/templates/stranice/magacin.html
index 7538d98..f2b5f6b 100644
--- a/web/templates/stranice/magacin.html
+++ b/web/templates/stranice/magacin.html
@@ -133,15 +133,7 @@
{{end}}
{{if index $.Dozvole "artikal.premesti"}}{{if $.Kategorije}}
-
- Premesti
-
-
+ {{template "premestiMeni" (dict "ID" .ID "Kategorije" $.Kategorije)}}
{{end}}{{end}}
{{if index $.Dozvole "artikal.obrisi"}}
Izmeni
{{end}}
{{if index $.Dozvole "artikal.premesti"}}{{if $.Kategorije}}
-
- Premesti
-
-
+ {{template "premestiMeni" (dict "ID" .ID "Kategorije" $.Kategorije)}}
{{end}}{{end}}
{{if index $.Dozvole "artikal.obrisi"}}
{{end}}
+
+{{/* padajući meni za premeštanje artikla — prima dict {ID, Kategorije}; koristi se i u tabeli i u mobilnoj kartici */}}
+{{define "premestiMeni"}}
+
+ Premesti
+
+
+{{end}}