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 -
- {{range $.Kategorije}} - - {{end}} - -
-
+ {{template "premestiMeni" (dict "ID" .ID "Kategorije" $.Kategorije)}} {{end}}{{end}} {{if index $.Dozvole "artikal.obrisi"}} Izmeni {{end}} {{if index $.Dozvole "artikal.premesti"}}{{if $.Kategorije}} -
- Premesti -
- {{range $.Kategorije}} - - {{end}} - -
-
+ {{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 +
+ {{range .Kategorije}} + + {{end}} + +
+
+{{end}}