Code-review popravke: RequireDozvola middleware, validacija backupa, dedup partiala
Bezbednost / dozvole:
- Nov RequireDozvola(proveri, akcija) middleware (po uzoru na RequireAdmin):
na odbijanje redirekt na /dashboard sa flash porukom umesto golog 403
- 10 "pregled" ruta (prodaja, servis, klijenti, dobavljači, nabavke) prešlo
na deklarativnu proveru na nivou rute
- Uklonjene inline pregled-provere iz handlera — provera je sad na jednom
mestu po ruti, vidljiva u ruteru
Backup podešavanja:
- Neispravan unos (van opsega ili ne-broj) više se ne preskače tiho;
prikazuje se jasna greška i ne prikazuje se lažno "sačuvano"
- Hvata se greška pri čuvanju u bazu (uklonjen progutani _ =)
Šabloni:
- "Premesti" dropdown izvučen u jedan {{define "premestiMeni"}} partial
(više nije dupliran u tabeli i mobilnoj kartici)
- Dodat dict helper u FuncMap saSidebar šablona radi prosleđivanja više
vrednosti partialu; standalone šabloni namerno ostaju bez izmene
This commit is contained in:
@@ -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)
|
||||
|
||||
+22
-2
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user