Flash poruke: toast notifikacije umesto ?greska= i ?sacuvano= u URL-u

This commit is contained in:
2026-06-05 23:15:49 +02:00
parent 2b3636528f
commit 9af712edd3
10 changed files with 267 additions and 116 deletions
+114 -83
View File
@@ -15,8 +15,6 @@ import (
type podaciAdminKorisnici struct { type podaciAdminKorisnici struct {
model.PodaciStranice model.PodaciStranice
Korisnici []model.Korisnik Korisnici []model.Korisnik
Greska string
Sacuvano bool
} }
type podaciLoginIstorija struct { type podaciLoginIstorija struct {
@@ -27,11 +25,11 @@ type podaciLoginIstorija struct {
type podaciAdminProfil struct { type podaciAdminProfil struct {
model.PodaciStranice model.PodaciStranice
Greska string // Greska se koristi samo za inline prikaz greške pri TOTP aktivaciji (bez redirecta)
Sacuvano string Greska string
TotpURI string TotpURI string
TotpTajna string TotpTajna string
TotpQR template.URL TotpQR template.URL
TotpAktivan bool TotpAktivan bool
} }
@@ -65,14 +63,10 @@ func (h *Handler) AdminKorisnici(w http.ResponseWriter, r *http.Request) {
ps.Stranica = "admin" ps.Stranica = "admin"
ps.NaslovStranice = "Korisnici" ps.NaslovStranice = "Korisnici"
podaci := podaciAdminKorisnici{ h.renderujTemplate(w, "admin_korisnici", podaciAdminKorisnici{
PodaciStranice: ps, PodaciStranice: ps,
Korisnici: lista, 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 // AdminSacuvajKorisnika kreira novog korisnika
@@ -84,7 +78,8 @@ func (h *Handler) AdminSacuvajKorisnika(w http.ResponseWriter, r *http.Request)
} }
if err := r.ParseForm(); err != nil { 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 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 // superadmin uloga se ne može kreirati kroz interfejs — jedini superadmin postoji od setup-a
validneUloge := map[string]bool{"admin": true, "radnik": true} validneUloge := map[string]bool{"admin": true, "radnik": true}
if len(ime) < 3 || len(lozinka) < 8 || !validneUloge[uloga] { 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 return
} }
hash, err := auth.HashujLozinku(lozinka) hash, err := auth.HashujLozinku(lozinka)
if err != nil { 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 return
} }
if _, err := h.KorisniciRepo.Kreiraj(r.Context(), ime, hash, uloga); err != nil { 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 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 // 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")) id, err := parseID(chi.URLParam(r, "id"))
if err != nil { 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 return
} }
// ne sme deaktivirati sam sebe // ne sme deaktivirati sam sebe
if id == k.ID { 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 return
} }
korisnik, err := h.KorisniciRepo.DohvatiPoID(r.Context(), id) korisnik, err := h.KorisniciRepo.DohvatiPoID(r.Context(), id)
if err != nil { 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 return
} }
// admin ne sme da menja status drugog admina ni superadmina // admin ne sme da menja status drugog admina ni superadmina
if korisnik.Uloga == "superadmin" || (korisnik.Uloga == "admin" && k.Uloga != "superadmin") { 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 return
} }
if err := h.KorisniciRepo.AzurirajAktivan(r.Context(), id, !korisnik.Aktivan); err != nil { 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 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 // 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")) id, err := parseID(chi.URLParam(r, "id"))
if err != nil { 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 return
} }
if err := r.ParseForm(); err != nil { 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 return
} }
// superadmin ne može menjati svoju vlastitu ulogu // superadmin ne može menjati svoju vlastitu ulogu
if id == k.ID { 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 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 // 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} validneUloge := map[string]bool{"admin": true, "radnik": true}
if !validneUloge[uloga] { 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 return
} }
// dohvati korisnika da proverimo njegovu trenutnu ulogu // dohvati korisnika da proverimo njegovu trenutnu ulogu
ciljniKorisnik, err := h.KorisniciRepo.DohvatiPoID(r.Context(), id) ciljniKorisnik, err := h.KorisniciRepo.DohvatiPoID(r.Context(), id)
if err != nil { 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 return
} }
// superadmin uloga se ne može menjati // superadmin uloga se ne može menjati
if ciljniKorisnik.Uloga == "superadmin" { 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 return
} }
if err := h.KorisniciRepo.AzurirajUlogu(r.Context(), id, uloga); err != nil { 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 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 // 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")) id, err := parseID(chi.URLParam(r, "id"))
if err != nil { 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 return
} }
if id == k.ID { 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 return
} }
ciljni, err := h.KorisniciRepo.DohvatiPoID(r.Context(), id) ciljni, err := h.KorisniciRepo.DohvatiPoID(r.Context(), id)
if err != nil { 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 return
} }
// dozvoljeno je brisanje samo radnika // dozvoljeno je brisanje samo radnika
if ciljni.Uloga != "radnik" { 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 return
} }
if err := h.KorisniciRepo.Obrisi(r.Context(), id); err != nil { 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 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 // AdminProfil prikazuje stranicu profila
@@ -265,14 +284,10 @@ func (h *Handler) AdminProfil(w http.ResponseWriter, r *http.Request) {
ps.Stranica = "profil" ps.Stranica = "profil"
ps.NaslovStranice = "Moj profil" ps.NaslovStranice = "Moj profil"
podaci := podaciAdminProfil{ h.renderujTemplate(w, "admin_profil", podaciAdminProfil{
PodaciStranice: ps, PodaciStranice: ps,
Greska: r.URL.Query().Get("greska"),
Sacuvano: r.URL.Query().Get("sacuvano"),
TotpAktivan: svezi.TotpTajna != "", TotpAktivan: svezi.TotpTajna != "",
} })
h.renderujTemplate(w, "admin_profil", podaci)
} }
// AdminPromeniLozinku menja lozinku prijavljenog korisnika // 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 { 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 return
} }
@@ -293,26 +309,31 @@ func (h *Handler) AdminPromeniLozinku(w http.ResponseWriter, r *http.Request) {
potvrda := r.FormValue("nova_lozinka_potvrda") potvrda := r.FormValue("nova_lozinka_potvrda")
if !auth.ProveriLozinku(k.LozinkaHash, stara) { 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 return
} }
if len(nova) < 8 || nova != potvrda { 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 return
} }
hash, err := auth.HashujLozinku(nova) hash, err := auth.HashujLozinku(nova)
if err != nil { 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 return
} }
if err := h.KorisniciRepo.PromeniLozinku(r.Context(), k.ID, hash); err != nil { 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 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 // 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) podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
totp, err := auth.GenerisuTotpTajnu(k.KorisnickoIme, podesavanja["naziv_firme"]) totp, err := auth.GenerisuTotpTajnu(k.KorisnickoIme, podesavanja["naziv_firme"])
if err != nil { 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 return
} }
@@ -334,14 +356,12 @@ func (h *Handler) AdminTotpPokreni(w http.ResponseWriter, r *http.Request) {
ps.Stranica = "profil" ps.Stranica = "profil"
ps.NaslovStranice = "Podesi 2FA" ps.NaslovStranice = "Podesi 2FA"
podaci := podaciAdminProfil{ h.renderujTemplate(w, "admin_profil", podaciAdminProfil{
PodaciStranice: ps, PodaciStranice: ps,
TotpURI: totp.URI, TotpURI: totp.URI,
TotpTajna: totp.Tajna, TotpTajna: totp.Tajna,
TotpQR: template.URL("data:image/png;base64," + totp.QRBase64), TotpQR: template.URL("data:image/png;base64," + totp.QRBase64),
} })
h.renderujTemplate(w, "admin_profil", podaci)
} }
// AdminTotpAktivacija verifikuje TOTP kod i čuva tajnu // 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 { 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 return
} }
@@ -361,36 +382,36 @@ func (h *Handler) AdminTotpAktivacija(w http.ResponseWriter, r *http.Request) {
kod := r.FormValue("kod") kod := r.FormValue("kod")
if !auth.VerifikujTotpKod(kod, tajna) { 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) podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
ps := h.popuniPodaciStranice(r, podesavanja) ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "profil" ps.Stranica = "profil"
ps.NaslovStranice = "Podesi 2FA" ps.NaslovStranice = "Podesi 2FA"
// regenerišemo QR za već generisanu tajnu (korisnik je video ovaj QR)
nazivFirme := podesavanja["naziv_firme"] nazivFirme := podesavanja["naziv_firme"]
if nazivFirme == "" { if nazivFirme == "" {
nazivFirme = "NTech" nazivFirme = "NTech"
} }
uri, qr := auth.RegenerisiTotpQR(tajna, k.KorisnickoIme, nazivFirme) uri, qr := auth.RegenerisiTotpQR(tajna, k.KorisnickoIme, nazivFirme)
podaci := podaciAdminProfil{ h.renderujTemplate(w, "admin_profil", podaciAdminProfil{
PodaciStranice: ps, PodaciStranice: ps,
TotpURI: uri, TotpURI: uri,
TotpTajna: tajna, TotpTajna: tajna,
TotpQR: template.URL("data:image/png;base64," + qr), TotpQR: template.URL("data:image/png;base64," + qr),
Greska: "totp", Greska: "totp",
} })
h.renderujTemplate(w, "admin_profil", podaci)
return return
} }
if err := h.KorisniciRepo.SacuvajTotpTajnu(r.Context(), k.ID, tajna); err != nil { 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 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 // 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 { 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 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 // AdminLoginIstorija prikazuje evidenciju prijava za datog korisnika
@@ -460,8 +483,6 @@ type podaciAdminDozvole struct {
DozvoleRadnik map[string]bool DozvoleRadnik map[string]bool
DozvoleAdmin map[string]bool DozvoleAdmin map[string]bool
DozvoleSuperadmin map[string]bool DozvoleSuperadmin map[string]bool
Greska string
Sacuvano bool
} }
// AdminDozvole prikazuje stranicu za upravljanje ulogama i pregled dozvola // 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"), DozvoleRadnik: h.DozvoleRepo.SveDozvole(r.Context(), "radnik"),
DozvoleAdmin: h.DozvoleRepo.SveDozvole(r.Context(), "admin"), DozvoleAdmin: h.DozvoleRepo.SveDozvole(r.Context(), "admin"),
DozvoleSuperadmin: h.DozvoleRepo.SveDozvole(r.Context(), "superadmin"), 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")) id, err := parseID(chi.URLParam(r, "id"))
if err != nil { 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 return
} }
if err := r.ParseForm(); err != nil { 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 return
} }
// superadmin ne može menjati svoju vlastitu ulogu // superadmin ne može menjati svoju vlastitu ulogu
if id == k.ID { 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 return
} }
@@ -524,28 +546,33 @@ func (h *Handler) AdminDozvolePromeniUlogu(w http.ResponseWriter, r *http.Reques
// superadmin uloga se ne može dodeliti kroz interfejs // superadmin uloga se ne može dodeliti kroz interfejs
validneUloge := map[string]bool{"admin": true, "radnik": true} validneUloge := map[string]bool{"admin": true, "radnik": true}
if !validneUloge[uloga] { 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 return
} }
ciljni, err := h.KorisniciRepo.DohvatiPoID(r.Context(), id) ciljni, err := h.KorisniciRepo.DohvatiPoID(r.Context(), id)
if err != nil { 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 return
} }
// superadmin uloga se ne može menjati // superadmin uloga se ne može menjati
if ciljni.Uloga == "superadmin" { 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 return
} }
if err := h.KorisniciRepo.AzurirajUlogu(r.Context(), id, uloga); err != nil { 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 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 // 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 return
} }
if err := r.ParseForm(); err != nil { 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 return
} }
// čuvamo dozvole samo za radnik i admin — superadmin uvek ima sve // č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 kljuc := uloga + "__" + akcija
dozvoljeno := r.FormValue(kljuc) == "on" dozvoljeno := r.FormValue(kljuc) == "on"
if err := h.DozvoleRepo.Sacuvaj(r.Context(), uloga, akcija, dozvoljeno); err != nil { 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 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 // 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 return
} }
if err := h.DozvoleRepo.Reset(r.Context()); err != nil { 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 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)
} }
+1
View File
@@ -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.Dozvole = h.DozvoleRepo.SveDozvole(r.Context(), k.Uloga)
} }
ps.CsrfToken = middleware.CsrfToken(r.Context()) ps.CsrfToken = middleware.CsrfToken(r.Context())
ps.Flash = middleware.GetFlash(r, h.DB)
return ps return ps
} }
+48
View File
@@ -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
}
+7
View File
@@ -23,6 +23,12 @@ type StavkaProdajePregled struct {
Datum string // kratki format, npr. "01.06." 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 // PodaciStranice su zajednički podaci koje svaka stranica prima
type PodaciStranice struct { type PodaciStranice struct {
Stranica string Stranica string
@@ -37,6 +43,7 @@ type PodaciStranice struct {
KorisnikUloga string // uloga: "superadmin", "admin", "radnik" KorisnikUloga string // uloga: "superadmin", "admin", "radnik"
CsrfToken string // CSRF zaštitni token za forme CsrfToken string // CSRF zaštitni token za forme
Dozvole map[string]bool // mapa akcija → dozvoljeno/nije Dozvole map[string]bool // mapa akcija → dozvoljeno/nije
Flash *FlashPoruka // jednokratna poruka nakon redirecta
} }
// PodaciDashboarda su podaci specifični za dashboard stranicu // PodaciDashboarda su podaci specifični za dashboard stranicu
+1
View File
@@ -0,0 +1 @@
ALTER TABLE sesije ADD COLUMN flash TEXT;
+77
View File
@@ -583,3 +583,80 @@ select {
min-width: 200px; min-width: 200px;
flex-wrap: wrap; 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;
}
}
@@ -30,13 +30,6 @@
{{define "sadrzaj"}} {{define "sadrzaj"}}
<div style="display:flex;flex-direction:column;gap:20px;"> <div style="display:flex;flex-direction:column;gap:20px;">
{{if .Sacuvano}}
<div class="poruka-uspeh">Promene su uspešno sačuvane.</div>
{{end}}
{{if .Greska}}
<div class="poruka-greska">Greška pri čuvanju promena.</div>
{{end}}
<!-- matrica dozvola — editabilna forma --> <!-- matrica dozvola — editabilna forma -->
<div class="kartica animiraj" style="padding:0;overflow:hidden;"> <div class="kartica animiraj" style="padding:0;overflow:hidden;">
<div style="padding:14px 16px;border-bottom:0.5px solid var(--ivica);"> <div style="padding:14px 16px;border-bottom:0.5px solid var(--ivica);">
@@ -26,18 +26,6 @@
{{define "sadrzaj"}} {{define "sadrzaj"}}
<div style="display:flex;flex-direction:column;gap:16px;"> <div style="display:flex;flex-direction:column;gap:16px;">
{{if .Sacuvano}}
<div class="poruka-uspeh poruka-animacija">Promene su uspešno sačuvane.</div>
{{end}}
{{if eq .Greska "1"}}
<div class="poruka-greska">Proverite unete podatke.</div>
{{else if eq .Greska "2"}}
<div class="poruka-greska">Greška pri čuvanju. Pokušajte ponovo.</div>
{{else if eq .Greska "3"}}
<div class="poruka-greska">Ova radnja nije dozvoljena.</div>
{{end}}
<!-- lista korisnika --> <!-- lista korisnika -->
<div class="kartica animiraj" style="padding:0;overflow:hidden;"> <div class="kartica animiraj" style="padding:0;overflow:hidden;">
<div style="padding:16px 20px;border-bottom:0.5px solid var(--ivica);"> <div style="padding:16px 20px;border-bottom:0.5px solid var(--ivica);">
-14
View File
@@ -14,26 +14,12 @@
{{define "sadrzaj"}} {{define "sadrzaj"}}
<div style="display:flex;flex-direction:column;gap:16px;max-width:560px;"> <div style="display:flex;flex-direction:column;gap:16px;max-width:560px;">
{{if eq .Sacuvano "lozinka"}}
<div class="poruka-uspeh">Lozinka je uspešno promenjena.</div>
{{else if eq .Sacuvano "totp"}}
<div class="poruka-uspeh">Dvostepena verifikacija je uspešno uključena.</div>
{{else if eq .Sacuvano "totp_off"}}
<div class="poruka-uspeh">Dvostepena verifikacija je isključena.</div>
{{end}}
<!-- promena lozinke --> <!-- promena lozinke -->
<div class="kartica animiraj"> <div class="kartica animiraj">
<div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);"> <div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
Promena lozinke Promena lozinke
</div> </div>
{{if eq .Greska "lozinka"}}
<div class="poruka-greska" style="margin-bottom:14px;">Stara lozinka nije ispravna.</div>
{{else if eq .Greska "lozinka2"}}
<div class="poruka-greska" style="margin-bottom:14px;">Nova lozinka mora imati najmanje 8 karaktera i lozinke moraju biti iste.</div>
{{end}}
<form method="POST" action="/admin/profil/lozinka"> <form method="POST" action="/admin/profil/lozinka">
<div style="display:flex;flex-direction:column;gap:12px;"> <div style="display:flex;flex-direction:column;gap:12px;">
<div> <div>
@@ -36,6 +36,25 @@
</div> </div>
</div> </div>
<!-- flash poruka — prikazuje se kao toast u gornjem desnom uglu -->
{{if .Flash}}
<div id="flash-toast" class="flash-toast flash-{{.Flash.Tip}}" role="alert">
<span class="flash-ikona">{{if eq .Flash.Tip "uspeh"}}✓{{else}}!{{end}}</span>
<span class="flash-tekst">{{.Flash.Poruka}}</span>
<button class="flash-zatvori" onclick="this.parentElement.remove()" aria-label="Zatvori">×</button>
</div>
<script>
(function() {
var t = document.getElementById('flash-toast');
if (!t) return;
setTimeout(function() {
t.classList.add('flash-izlaz');
setTimeout(function() { if (t.parentElement) t.remove(); }, 350);
}, 4000);
})();
</script>
{{end}}
<!-- alpine.js za interaktivnost --> <!-- alpine.js za interaktivnost -->
<script <script
src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"