diff --git a/internal/handler/admin.go b/internal/handler/admin.go index 1416e4c..dc69163 100644 --- a/internal/handler/admin.go +++ b/internal/handler/admin.go @@ -15,8 +15,6 @@ import ( type podaciAdminKorisnici struct { model.PodaciStranice Korisnici []model.Korisnik - Greska string - Sacuvano bool } type podaciLoginIstorija struct { @@ -27,11 +25,11 @@ type podaciLoginIstorija struct { type podaciAdminProfil struct { model.PodaciStranice - Greska string - Sacuvano string - TotpURI string - TotpTajna string - TotpQR template.URL + // Greska se koristi samo za inline prikaz greške pri TOTP aktivaciji (bez redirecta) + Greska string + TotpURI string + TotpTajna string + TotpQR template.URL TotpAktivan bool } @@ -65,14 +63,10 @@ func (h *Handler) AdminKorisnici(w http.ResponseWriter, r *http.Request) { ps.Stranica = "admin" ps.NaslovStranice = "Korisnici" - podaci := podaciAdminKorisnici{ + h.renderujTemplate(w, "admin_korisnici", podaciAdminKorisnici{ PodaciStranice: ps, Korisnici: lista, - Greska: r.URL.Query().Get("greska"), - Sacuvano: r.URL.Query().Get("sacuvano") == "1", - } - - h.renderujTemplate(w, "admin_korisnici", podaci) + }) } // AdminSacuvajKorisnika kreira novog korisnika @@ -84,7 +78,8 @@ func (h *Handler) AdminSacuvajKorisnika(w http.ResponseWriter, r *http.Request) } if err := r.ParseForm(); err != nil { - http.Redirect(w, r, "/admin/korisnici?greska=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Proverite unete podatke.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } @@ -95,22 +90,26 @@ func (h *Handler) AdminSacuvajKorisnika(w http.ResponseWriter, r *http.Request) // superadmin uloga se ne može kreirati kroz interfejs — jedini superadmin postoji od setup-a validneUloge := map[string]bool{"admin": true, "radnik": true} if len(ime) < 3 || len(lozinka) < 8 || !validneUloge[uloga] { - http.Redirect(w, r, "/admin/korisnici?greska=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Proverite unete podatke.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } hash, err := auth.HashujLozinku(lozinka) if err != nil { - http.Redirect(w, r, "/admin/korisnici?greska=2", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } if _, err := h.KorisniciRepo.Kreiraj(r.Context(), ime, hash, uloga); err != nil { - http.Redirect(w, r, "/admin/korisnici?greska=2", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } - http.Redirect(w, r, "/admin/korisnici?sacuvano=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "uspeh", "Korisnik je uspešno kreiran.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) } // AdminToggleAktivan menja aktivan status korisnika @@ -123,34 +122,40 @@ func (h *Handler) AdminToggleAktivan(w http.ResponseWriter, r *http.Request) { id, err := parseID(chi.URLParam(r, "id")) if err != nil { - http.Redirect(w, r, "/admin/korisnici?greska=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Proverite unete podatke.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } // ne sme deaktivirati sam sebe if id == k.ID { - http.Redirect(w, r, "/admin/korisnici?greska=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Ne možete promeniti status sopstvenog naloga.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } korisnik, err := h.KorisniciRepo.DohvatiPoID(r.Context(), id) if err != nil { - http.Redirect(w, r, "/admin/korisnici?greska=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Proverite unete podatke.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } // admin ne sme da menja status drugog admina ni superadmina if korisnik.Uloga == "superadmin" || (korisnik.Uloga == "admin" && k.Uloga != "superadmin") { - http.Redirect(w, r, "/admin/korisnici?greska=3", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Ova radnja nije dozvoljena.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } if err := h.KorisniciRepo.AzurirajAktivan(r.Context(), id, !korisnik.Aktivan); err != nil { - http.Redirect(w, r, "/admin/korisnici?greska=2", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } - http.Redirect(w, r, "/admin/korisnici?sacuvano=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "uspeh", "Promene su uspešno sačuvane.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) } // AdminPromeniUlogu menja ulogu korisnika @@ -163,18 +168,21 @@ func (h *Handler) AdminPromeniUlogu(w http.ResponseWriter, r *http.Request) { id, err := parseID(chi.URLParam(r, "id")) if err != nil { - http.Redirect(w, r, "/admin/korisnici?greska=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Proverite unete podatke.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } if err := r.ParseForm(); err != nil { - http.Redirect(w, r, "/admin/korisnici?greska=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Proverite unete podatke.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } // superadmin ne može menjati svoju vlastitu ulogu if id == k.ID { - http.Redirect(w, r, "/admin/korisnici?greska=3", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Ova radnja nije dozvoljena.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } @@ -182,29 +190,34 @@ func (h *Handler) AdminPromeniUlogu(w http.ResponseWriter, r *http.Request) { // dozvoljena je samo promena između admin i radnik; superadmin uloga se ne može dodeliti ni ukloniti validneUloge := map[string]bool{"admin": true, "radnik": true} if !validneUloge[uloga] { - http.Redirect(w, r, "/admin/korisnici?greska=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Proverite unete podatke.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } // dohvati korisnika da proverimo njegovu trenutnu ulogu ciljniKorisnik, err := h.KorisniciRepo.DohvatiPoID(r.Context(), id) if err != nil { - http.Redirect(w, r, "/admin/korisnici?greska=2", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } // superadmin uloga se ne može menjati if ciljniKorisnik.Uloga == "superadmin" { - http.Redirect(w, r, "/admin/korisnici?greska=3", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Ova radnja nije dozvoljena.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } if err := h.KorisniciRepo.AzurirajUlogu(r.Context(), id, uloga); err != nil { - http.Redirect(w, r, "/admin/korisnici?greska=2", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } - http.Redirect(w, r, "/admin/korisnici?sacuvano=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "uspeh", "Promene su uspešno sačuvane.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) } // AdminObrisiKorisnika briše korisnika sa ulogom radnik @@ -217,33 +230,39 @@ func (h *Handler) AdminObrisiKorisnika(w http.ResponseWriter, r *http.Request) { id, err := parseID(chi.URLParam(r, "id")) if err != nil { - http.Redirect(w, r, "/admin/korisnici?greska=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Proverite unete podatke.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } if id == k.ID { - http.Redirect(w, r, "/admin/korisnici?greska=3", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Ova radnja nije dozvoljena.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } ciljni, err := h.KorisniciRepo.DohvatiPoID(r.Context(), id) if err != nil { - http.Redirect(w, r, "/admin/korisnici?greska=2", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } // dozvoljeno je brisanje samo radnika if ciljni.Uloga != "radnik" { - http.Redirect(w, r, "/admin/korisnici?greska=3", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Ova radnja nije dozvoljena.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } if err := h.KorisniciRepo.Obrisi(r.Context(), id); err != nil { - http.Redirect(w, r, "/admin/korisnici?greska=2", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) return } - http.Redirect(w, r, "/admin/korisnici?sacuvano=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "uspeh", "Korisnik je obrisan.") + http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther) } // AdminProfil prikazuje stranicu profila @@ -265,14 +284,10 @@ func (h *Handler) AdminProfil(w http.ResponseWriter, r *http.Request) { ps.Stranica = "profil" ps.NaslovStranice = "Moj profil" - podaci := podaciAdminProfil{ + h.renderujTemplate(w, "admin_profil", podaciAdminProfil{ PodaciStranice: ps, - Greska: r.URL.Query().Get("greska"), - Sacuvano: r.URL.Query().Get("sacuvano"), TotpAktivan: svezi.TotpTajna != "", - } - - h.renderujTemplate(w, "admin_profil", podaci) + }) } // AdminPromeniLozinku menja lozinku prijavljenog korisnika @@ -284,7 +299,8 @@ func (h *Handler) AdminPromeniLozinku(w http.ResponseWriter, r *http.Request) { } if err := r.ParseForm(); err != nil { - http.Redirect(w, r, "/admin/profil?greska=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Greška. Pokušajte ponovo.") + http.Redirect(w, r, "/admin/profil", http.StatusSeeOther) return } @@ -293,26 +309,31 @@ func (h *Handler) AdminPromeniLozinku(w http.ResponseWriter, r *http.Request) { potvrda := r.FormValue("nova_lozinka_potvrda") if !auth.ProveriLozinku(k.LozinkaHash, stara) { - http.Redirect(w, r, "/admin/profil?greska=lozinka", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Stara lozinka nije ispravna.") + http.Redirect(w, r, "/admin/profil", http.StatusSeeOther) return } if len(nova) < 8 || nova != potvrda { - http.Redirect(w, r, "/admin/profil?greska=lozinka2", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Nova lozinka mora imati najmanje 8 karaktera i lozinke moraju biti iste.") + http.Redirect(w, r, "/admin/profil", http.StatusSeeOther) return } hash, err := auth.HashujLozinku(nova) if err != nil { - http.Redirect(w, r, "/admin/profil?greska=2", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.") + http.Redirect(w, r, "/admin/profil", http.StatusSeeOther) return } if err := h.KorisniciRepo.PromeniLozinku(r.Context(), k.ID, hash); err != nil { - http.Redirect(w, r, "/admin/profil?greska=2", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.") + http.Redirect(w, r, "/admin/profil", http.StatusSeeOther) return } - http.Redirect(w, r, "/admin/profil?sacuvano=lozinka", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "uspeh", "Lozinka je uspešno promenjena.") + http.Redirect(w, r, "/admin/profil", http.StatusSeeOther) } // AdminTotpPokreni generiše TOTP tajnu i prikazuje QR kod @@ -326,7 +347,8 @@ func (h *Handler) AdminTotpPokreni(w http.ResponseWriter, r *http.Request) { podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) totp, err := auth.GenerisuTotpTajnu(k.KorisnickoIme, podesavanja["naziv_firme"]) if err != nil { - http.Redirect(w, r, "/admin/profil?greska=2", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Greška pri generisanju 2FA. Pokušajte ponovo.") + http.Redirect(w, r, "/admin/profil", http.StatusSeeOther) return } @@ -334,14 +356,12 @@ func (h *Handler) AdminTotpPokreni(w http.ResponseWriter, r *http.Request) { ps.Stranica = "profil" ps.NaslovStranice = "Podesi 2FA" - podaci := podaciAdminProfil{ + h.renderujTemplate(w, "admin_profil", podaciAdminProfil{ PodaciStranice: ps, TotpURI: totp.URI, TotpTajna: totp.Tajna, TotpQR: template.URL("data:image/png;base64," + totp.QRBase64), - } - - h.renderujTemplate(w, "admin_profil", podaci) + }) } // AdminTotpAktivacija verifikuje TOTP kod i čuva tajnu @@ -353,7 +373,8 @@ func (h *Handler) AdminTotpAktivacija(w http.ResponseWriter, r *http.Request) { } if err := r.ParseForm(); err != nil { - http.Redirect(w, r, "/admin/profil?greska=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Greška. Pokušajte ponovo.") + http.Redirect(w, r, "/admin/profil", http.StatusSeeOther) return } @@ -361,36 +382,36 @@ func (h *Handler) AdminTotpAktivacija(w http.ResponseWriter, r *http.Request) { kod := r.FormValue("kod") if !auth.VerifikujTotpKod(kod, tajna) { - // ponovni prikaz sa greškom — regenerišemo isti tajnu + // ponovni prikaz sa greškom — regenerišemo isti QR, ne radimo redirect podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) ps := h.popuniPodaciStranice(r, podesavanja) ps.Stranica = "profil" ps.NaslovStranice = "Podesi 2FA" - // regenerišemo QR za već generisanu tajnu (korisnik je video ovaj QR) nazivFirme := podesavanja["naziv_firme"] if nazivFirme == "" { nazivFirme = "NTech" } uri, qr := auth.RegenerisiTotpQR(tajna, k.KorisnickoIme, nazivFirme) - podaci := podaciAdminProfil{ + h.renderujTemplate(w, "admin_profil", podaciAdminProfil{ PodaciStranice: ps, TotpURI: uri, TotpTajna: tajna, TotpQR: template.URL("data:image/png;base64," + qr), Greska: "totp", - } - h.renderujTemplate(w, "admin_profil", podaci) + }) return } if err := h.KorisniciRepo.SacuvajTotpTajnu(r.Context(), k.ID, tajna); err != nil { - http.Redirect(w, r, "/admin/profil?greska=2", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.") + http.Redirect(w, r, "/admin/profil", http.StatusSeeOther) return } - http.Redirect(w, r, "/admin/profil?sacuvano=totp", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "uspeh", "Dvostepena verifikacija je uspešno uključena.") + http.Redirect(w, r, "/admin/profil", http.StatusSeeOther) } // AdminTotpDeaktivacija uklanja TOTP tajnu @@ -402,11 +423,13 @@ func (h *Handler) AdminTotpDeaktivacija(w http.ResponseWriter, r *http.Request) } if err := h.KorisniciRepo.SacuvajTotpTajnu(r.Context(), k.ID, ""); err != nil { - http.Redirect(w, r, "/admin/profil?greska=2", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.") + http.Redirect(w, r, "/admin/profil", http.StatusSeeOther) return } - http.Redirect(w, r, "/admin/profil?sacuvano=totp_off", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "uspeh", "Dvostepena verifikacija je isključena.") + http.Redirect(w, r, "/admin/profil", http.StatusSeeOther) } // AdminLoginIstorija prikazuje evidenciju prijava za datog korisnika @@ -460,8 +483,6 @@ type podaciAdminDozvole struct { DozvoleRadnik map[string]bool DozvoleAdmin map[string]bool DozvoleSuperadmin map[string]bool - Greska string - Sacuvano bool } // AdminDozvole prikazuje stranicu za upravljanje ulogama i pregled dozvola @@ -490,8 +511,6 @@ func (h *Handler) AdminDozvole(w http.ResponseWriter, r *http.Request) { DozvoleRadnik: h.DozvoleRepo.SveDozvole(r.Context(), "radnik"), DozvoleAdmin: h.DozvoleRepo.SveDozvole(r.Context(), "admin"), DozvoleSuperadmin: h.DozvoleRepo.SveDozvole(r.Context(), "superadmin"), - Greska: r.URL.Query().Get("greska"), - Sacuvano: r.URL.Query().Get("sacuvano") == "1", }) } @@ -505,18 +524,21 @@ func (h *Handler) AdminDozvolePromeniUlogu(w http.ResponseWriter, r *http.Reques id, err := parseID(chi.URLParam(r, "id")) if err != nil { - http.Redirect(w, r, "/admin/dozvole?greska=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Proverite unete podatke.") + http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther) return } if err := r.ParseForm(); err != nil { - http.Redirect(w, r, "/admin/dozvole?greska=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Proverite unete podatke.") + http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther) return } // superadmin ne može menjati svoju vlastitu ulogu if id == k.ID { - http.Redirect(w, r, "/admin/dozvole?greska=3", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Ova radnja nije dozvoljena.") + http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther) return } @@ -524,28 +546,33 @@ func (h *Handler) AdminDozvolePromeniUlogu(w http.ResponseWriter, r *http.Reques // superadmin uloga se ne može dodeliti kroz interfejs validneUloge := map[string]bool{"admin": true, "radnik": true} if !validneUloge[uloga] { - http.Redirect(w, r, "/admin/dozvole?greska=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Proverite unete podatke.") + http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther) return } ciljni, err := h.KorisniciRepo.DohvatiPoID(r.Context(), id) if err != nil { - http.Redirect(w, r, "/admin/dozvole?greska=2", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.") + http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther) return } // superadmin uloga se ne može menjati if ciljni.Uloga == "superadmin" { - http.Redirect(w, r, "/admin/dozvole?greska=3", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Ova radnja nije dozvoljena.") + http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther) return } if err := h.KorisniciRepo.AzurirajUlogu(r.Context(), id, uloga); err != nil { - http.Redirect(w, r, "/admin/dozvole?greska=2", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.") + http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther) return } - http.Redirect(w, r, "/admin/dozvole?sacuvano=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "uspeh", "Promene su uspešno sačuvane.") + http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther) } // AdminDozvoleSacuvaj prima POST i čuva promene dozvola za radnik i admin uloge @@ -556,7 +583,8 @@ func (h *Handler) AdminDozvoleSacuvaj(w http.ResponseWriter, r *http.Request) { return } if err := r.ParseForm(); err != nil { - http.Redirect(w, r, "/admin/dozvole?greska=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Proverite unete podatke.") + http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther) return } // čuvamo dozvole samo za radnik i admin — superadmin uvek ima sve @@ -565,12 +593,14 @@ func (h *Handler) AdminDozvoleSacuvaj(w http.ResponseWriter, r *http.Request) { kljuc := uloga + "__" + akcija dozvoljeno := r.FormValue(kljuc) == "on" if err := h.DozvoleRepo.Sacuvaj(r.Context(), uloga, akcija, dozvoljeno); err != nil { - http.Redirect(w, r, "/admin/dozvole?greska=2", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.") + http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther) return } } } - http.Redirect(w, r, "/admin/dozvole?sacuvano=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "uspeh", "Promene su uspešno sačuvane.") + http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther) } // AdminDozvoleReset vraća sve dozvole na podrazumevane vrednosti — samo superadmin @@ -581,9 +611,10 @@ func (h *Handler) AdminDozvoleReset(w http.ResponseWriter, r *http.Request) { return } if err := h.DozvoleRepo.Reset(r.Context()); err != nil { - http.Redirect(w, r, "/admin/dozvole?greska=2", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.") + http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther) return } - http.Redirect(w, r, "/admin/dozvole?sacuvano=1", http.StatusSeeOther) + middleware.SetFlash(w, r, h.DB, "uspeh", "Dozvole su vraćene na podrazumevane vrednosti.") + http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther) } - diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 535155d..bb33f60 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -89,5 +89,6 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s ps.Dozvole = h.DozvoleRepo.SveDozvole(r.Context(), k.Uloga) } ps.CsrfToken = middleware.CsrfToken(r.Context()) + ps.Flash = middleware.GetFlash(r, h.DB) return ps } diff --git a/internal/middleware/flash.go b/internal/middleware/flash.go new file mode 100644 index 0000000..c583a69 --- /dev/null +++ b/internal/middleware/flash.go @@ -0,0 +1,48 @@ +package middleware + +import ( + "database/sql" + "encoding/json" + "net/http" + + "ntech/internal/model" +) + +// SetFlash čuva flash poruku u koloni flash aktivne sesije +func SetFlash(w http.ResponseWriter, r *http.Request, db *sql.DB, tip, poruka string) { + kolacic, err := r.Cookie("ntech_sesija") + if err != nil { + return + } + data, err := json.Marshal(model.FlashPoruka{Tip: tip, Poruka: poruka}) + if err != nil { + return + } + db.ExecContext(r.Context(), + `UPDATE sesije SET flash = ? WHERE token = ?`, + string(data), kolacic.Value) +} + +// GetFlash čita i atomično briše flash poruku iz aktivne sesije +func GetFlash(r *http.Request, db *sql.DB) *model.FlashPoruka { + kolacic, err := r.Cookie("ntech_sesija") + if err != nil { + return nil + } + var flashJSON sql.NullString + if err := db.QueryRowContext(r.Context(), + `SELECT flash FROM sesije WHERE token = ?`, kolacic.Value).Scan(&flashJSON); err != nil { + return nil + } + if !flashJSON.Valid || flashJSON.String == "" { + return nil + } + // briše pre parsiranja — ako parsiranje ne uspe, poruka se svakako ne prikazuje + db.ExecContext(r.Context(), + `UPDATE sesije SET flash = NULL WHERE token = ?`, kolacic.Value) + var f model.FlashPoruka + if err := json.Unmarshal([]byte(flashJSON.String), &f); err != nil { + return nil + } + return &f +} diff --git a/internal/model/stranica.go b/internal/model/stranica.go index 45f3f79..0be0795 100644 --- a/internal/model/stranica.go +++ b/internal/model/stranica.go @@ -23,6 +23,12 @@ type StavkaProdajePregled struct { Datum string // kratki format, npr. "01.06." } +// FlashPoruka je jednokratna poruka koja se prikazuje korisniku nakon redirecta +type FlashPoruka struct { + Tip string // "uspeh" ili "greska" + Poruka string +} + // PodaciStranice su zajednički podaci koje svaka stranica prima type PodaciStranice struct { Stranica string @@ -37,6 +43,7 @@ type PodaciStranice struct { KorisnikUloga string // uloga: "superadmin", "admin", "radnik" CsrfToken string // CSRF zaštitni token za forme Dozvole map[string]bool // mapa akcija → dozvoljeno/nije + Flash *FlashPoruka // jednokratna poruka nakon redirecta } // PodaciDashboarda su podaci specifični za dashboard stranicu diff --git a/migrations/022_flash.sql b/migrations/022_flash.sql new file mode 100644 index 0000000..6c01200 --- /dev/null +++ b/migrations/022_flash.sql @@ -0,0 +1 @@ +ALTER TABLE sesije ADD COLUMN flash TEXT; diff --git a/web/static/css/main.css b/web/static/css/main.css index 2b15dc0..b4a8e85 100644 --- a/web/static/css/main.css +++ b/web/static/css/main.css @@ -583,3 +583,80 @@ select { min-width: 200px; flex-wrap: wrap; } + +/* flash toast — jednokratna poruka u gornjem desnom uglu */ +@keyframes flashUlaz { + from { opacity: 0; transform: translateX(20px); } + to { opacity: 1; transform: translateX(0); } +} +@keyframes flashIzlaz { + from { opacity: 1; transform: translateX(0); } + to { opacity: 0; transform: translateX(20px); } +} + +.flash-toast { + position: fixed; + top: 20px; + right: 20px; + z-index: 9999; + display: flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + border-radius: 10px; + font-size: 14px; + max-width: 380px; + box-shadow: 0 4px 16px rgba(0,0,0,0.12); + animation: flashUlaz 0.3s ease forwards; +} + +.flash-toast.flash-izlaz { + animation: flashIzlaz 0.35s ease forwards; +} + +.flash-toast.flash-uspeh { + background: #f0fdf4; + color: #15803d; + border: 0.5px solid #bbf7d0; +} + +.flash-toast.flash-greska { + background: #fef2f2; + color: #dc2626; + border: 0.5px solid #fecaca; +} + +.flash-ikona { + font-weight: 700; + font-size: 15px; + flex-shrink: 0; +} + +.flash-tekst { + flex: 1; + line-height: 1.4; +} + +.flash-zatvori { + background: none; + border: none; + cursor: pointer; + font-size: 18px; + line-height: 1; + color: inherit; + opacity: 0.6; + padding: 0 0 0 4px; + flex-shrink: 0; +} + +.flash-zatvori:hover { opacity: 1; } + +@media (max-width: 480px) { + .flash-toast { + top: auto; + bottom: 20px; + right: 12px; + left: 12px; + max-width: none; + } +} diff --git a/web/templates/stranice/admin_dozvole.html b/web/templates/stranice/admin_dozvole.html index 40387d3..f2aeaa6 100644 --- a/web/templates/stranice/admin_dozvole.html +++ b/web/templates/stranice/admin_dozvole.html @@ -30,13 +30,6 @@ {{define "sadrzaj"}}
- {{if .Sacuvano}} -
Promene su uspešno sačuvane.
- {{end}} - {{if .Greska}} -
Greška pri čuvanju promena.
- {{end}} -
diff --git a/web/templates/stranice/admin_korisnici.html b/web/templates/stranice/admin_korisnici.html index 66508c2..5878ce4 100644 --- a/web/templates/stranice/admin_korisnici.html +++ b/web/templates/stranice/admin_korisnici.html @@ -26,18 +26,6 @@ {{define "sadrzaj"}}
- {{if .Sacuvano}} -
Promene su uspešno sačuvane.
- {{end}} - - {{if eq .Greska "1"}} -
Proverite unete podatke.
- {{else if eq .Greska "2"}} -
Greška pri čuvanju. Pokušajte ponovo.
- {{else if eq .Greska "3"}} -
Ova radnja nije dozvoljena.
- {{end}} -
diff --git a/web/templates/stranice/admin_profil.html b/web/templates/stranice/admin_profil.html index 5a04ebe..6835555 100644 --- a/web/templates/stranice/admin_profil.html +++ b/web/templates/stranice/admin_profil.html @@ -14,26 +14,12 @@ {{define "sadrzaj"}}
- {{if eq .Sacuvano "lozinka"}} -
Lozinka je uspešno promenjena.
- {{else if eq .Sacuvano "totp"}} -
Dvostepena verifikacija je uspešno uključena.
- {{else if eq .Sacuvano "totp_off"}} -
Dvostepena verifikacija je isključena.
- {{end}} -
Promena lozinke
- {{if eq .Greska "lozinka"}} -
Stara lozinka nije ispravna.
- {{else if eq .Greska "lozinka2"}} -
Nova lozinka mora imati najmanje 8 karaktera i lozinke moraju biti iste.
- {{end}} -
diff --git a/web/templates/teme/podrazumevana/base.html b/web/templates/teme/podrazumevana/base.html index cac0193..6f2f196 100644 --- a/web/templates/teme/podrazumevana/base.html +++ b/web/templates/teme/podrazumevana/base.html @@ -36,6 +36,25 @@
+ + {{if .Flash}} + + + {{end}} +