Ispravke: ugnježdena forma, greška čitanja fajla, mrtvi flag, refaktor struct

- podesavanja_opste.html: forma za uklanjanje loga premešena van forme za
  otpremanje — ugnježdene forme su nevažeći HTML
- ProfilOtpremiAvatar: greška iz fajl.Read(buf) se sada proverava (dozvoljava
  io.EOF, odbacuje pravi problem čitanja)
- TopbarLogoTekst uklonjen: iz modela, oba handlera i struct-a u podesavanja.go
  (podešavanje nije korišćeno ni u jednom šablonu)
- korisnici.go: dodeliOpcijeKorisnika prima korisnikOpcije struct umesto dugačke
  liste parametara; skeniraiKorisnika i Lista ažurirani
This commit is contained in:
2026-06-16 03:00:38 +02:00
parent 85cb1e25c7
commit 4fe6d53bf9
6 changed files with 70 additions and 74 deletions
+41 -40
View File
@@ -17,43 +17,48 @@ type sqliteKorisniciRepo struct {
kljuc []byte kljuc []byte
} }
// dodeliOpcijeKorisnika popunjava bool i opciona polja korisnika iz skeniranih // korisnikOpcije drži NULL vrednosti skeniranih opcionalnih kolona korisnika;
// NULL vrednosti — deljeno između skeniraiKorisnika (jedan red) i Lista (više redova) // dodavanje novog polja zahteva izmenu samo ovog struct-a i relevantnih Scan poziva.
func dodeliOpcijeKorisnika(k *model.Korisnik, aktivan, koristiLokalnuTemu int, type korisnikOpcije struct {
lokalnaTema, lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, aktivan int
lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity, avatarPutanja sql.NullString, koristiLokalnuTemu int
datumKreiranja time.Time) { datumKreiranja time.Time
k.Aktivan = aktivan == 1 lokalnaTema sql.NullString
k.LokalnaTema = lokalnaTema.String lokalnaPozadina sql.NullString
k.KoristiLokalnuTemu = koristiLokalnuTemu == 1 lokalnaPozadinaOpacity sql.NullString
k.DatumKreiranja = datumKreiranja lokalnaPozadinaBlur sql.NullString
k.LokalnaPozadina = lokalnaPozadina.String lokalnaPozadinaBlurPozadine sql.NullString
k.LokalnaPozadinaOpacity = lokalnaPozadinaOpacity.String lokalnaPozadinaGlassOpacity sql.NullString
k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String avatarPutanja sql.NullString
k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String }
k.LokalnaPozadinaGlassOpacity = lokalnaPozadinaGlassOpacity.String
k.AvatarPutanja = avatarPutanja.String // dodeliOpcijeKorisnika prenosi vrednosti iz korisnikOpcije na model.Korisnik
func dodeliOpcijeKorisnika(k *model.Korisnik, o korisnikOpcije) {
k.Aktivan = o.aktivan == 1
k.KoristiLokalnuTemu = o.koristiLokalnuTemu == 1
k.DatumKreiranja = o.datumKreiranja
k.LokalnaTema = o.lokalnaTema.String
k.LokalnaPozadina = o.lokalnaPozadina.String
k.LokalnaPozadinaOpacity = o.lokalnaPozadinaOpacity.String
k.LokalnaPozadinaBlur = o.lokalnaPozadinaBlur.String
k.LokalnaPozadinaBlurPozadine = o.lokalnaPozadinaBlurPozadine.String
k.LokalnaPozadinaGlassOpacity = o.lokalnaPozadinaGlassOpacity.String
k.AvatarPutanja = o.avatarPutanja.String
} }
// skeniraiKorisnika čita jedan red iz baze i popunjava model.Korisnik // skeniraiKorisnika čita jedan red iz baze i popunjava model.Korisnik
func skeniraiKorisnika(row interface{ Scan(...any) error }) (*model.Korisnik, error) { func skeniraiKorisnika(row interface{ Scan(...any) error }) (*model.Korisnik, error) {
k := &model.Korisnik{} k := &model.Korisnik{}
var aktivan, koristiLokalnuTemu int var o korisnikOpcije
var lokalnaTema sql.NullString
var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity sql.NullString
var avatarPutanja sql.NullString
var datumKreiranja time.Time
if err := row.Scan( if err := row.Scan(
&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &k.TotpTajna, &k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &o.aktivan, &k.TotpTajna,
&lokalnaTema, &koristiLokalnuTemu, &datumKreiranja, &o.lokalnaTema, &o.koristiLokalnuTemu, &o.datumKreiranja,
&lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &o.lokalnaPozadina, &o.lokalnaPozadinaOpacity, &o.lokalnaPozadinaBlur,
&lokalnaPozadinaBlurPozadine, &lokalnaPozadinaGlassOpacity, &avatarPutanja, &o.lokalnaPozadinaBlurPozadine, &o.lokalnaPozadinaGlassOpacity, &o.avatarPutanja,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
dodeliOpcijeKorisnika(k, aktivan, koristiLokalnuTemu, lokalnaTema, dodeliOpcijeKorisnika(k, o)
lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur,
lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity, avatarPutanja, datumKreiranja)
return k, nil return k, nil
} }
@@ -134,20 +139,16 @@ func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, erro
var lista []model.Korisnik var lista []model.Korisnik
for rows.Next() { for rows.Next() {
var k model.Korisnik var k model.Korisnik
var aktivan, koristiLokalnuTemu int var o korisnikOpcije
var lokalnaTema sql.NullString if err := rows.Scan(
var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity sql.NullString &k.ID, &k.KorisnickoIme, &k.Uloga, &o.aktivan, &k.TotpTajna,
var avatarPutanja sql.NullString &o.lokalnaTema, &o.koristiLokalnuTemu, &o.datumKreiranja,
var datumKreiranja time.Time &o.lokalnaPozadina, &o.lokalnaPozadinaOpacity, &o.lokalnaPozadinaBlur,
if err := rows.Scan(&k.ID, &k.KorisnickoIme, &k.Uloga, &aktivan, &k.TotpTajna, &o.lokalnaPozadinaBlurPozadine, &o.lokalnaPozadinaGlassOpacity, &o.avatarPutanja,
&lokalnaTema, &koristiLokalnuTemu, &datumKreiranja, ); err != nil {
&lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &lokalnaPozadinaBlurPozadine,
&lokalnaPozadinaGlassOpacity, &avatarPutanja); err != nil {
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err) return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
} }
dodeliOpcijeKorisnika(&k, aktivan, koristiLokalnuTemu, lokalnaTema, dodeliOpcijeKorisnika(&k, o)
lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur,
lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity, avatarPutanja, datumKreiranja)
r.desifrujTotpTajnu(&k) r.desifrujTotpTajnu(&k)
lista = append(lista, k) lista = append(lista, k)
} }
-1
View File
@@ -166,7 +166,6 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
Podnazlov: podesavanja["podnazlov"], Podnazlov: podesavanja["podnazlov"],
LogoPutanja: podesavanja["logo_putanja"], LogoPutanja: podesavanja["logo_putanja"],
TopbarLogoSlika: podesavanja["topbar_logo_slika"] == "1", TopbarLogoSlika: podesavanja["topbar_logo_slika"] == "1",
TopbarLogoTekst: podesavanja["topbar_logo_tekst"] == "1",
Korisnik: "Admin", Korisnik: "Admin",
} }
var korisnik *model.Korisnik var korisnik *model.Korisnik
-8
View File
@@ -30,7 +30,6 @@ type PodaciPodesavanja struct {
PIB string PIB string
LogoPutanja string LogoPutanja string
TopbarLogoSlika bool TopbarLogoSlika bool
TopbarLogoTekst bool
// profil firme — pravni/poreski status (Faza 0); određuje koji se zakonski moduli pale // profil firme — pravni/poreski status (Faza 0); određuje koji se zakonski moduli pale
FirmaPravniOblik string FirmaPravniOblik string
FirmaPdvObveznik string FirmaPdvObveznik string
@@ -84,7 +83,6 @@ func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) {
PIB: podesavanja["pib"], PIB: podesavanja["pib"],
LogoPutanja: podesavanja["logo_putanja"], LogoPutanja: podesavanja["logo_putanja"],
TopbarLogoSlika: podesavanja["topbar_logo_slika"] == "1", TopbarLogoSlika: podesavanja["topbar_logo_slika"] == "1",
TopbarLogoTekst: podesavanja["topbar_logo_tekst"] == "1",
Sacuvano: r.URL.Query().Get("sacuvano") == "1", Sacuvano: r.URL.Query().Get("sacuvano") == "1",
BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno", BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno",
Verzija: h.Verzija, Verzija: h.Verzija,
@@ -240,10 +238,6 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
if r.FormValue("topbar_logo_slika") == "1" { if r.FormValue("topbar_logo_slika") == "1" {
topbarLogoSlika = "1" topbarLogoSlika = "1"
} }
topbarLogoTekst := "0"
if r.FormValue("topbar_logo_tekst") == "1" {
topbarLogoTekst = "1"
}
polja := map[string]string{ polja := map[string]string{
"naziv_firme": r.FormValue("naziv_firme"), "naziv_firme": r.FormValue("naziv_firme"),
@@ -252,7 +246,6 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
"telefon": r.FormValue("telefon"), "telefon": r.FormValue("telefon"),
"pib": r.FormValue("pib"), "pib": r.FormValue("pib"),
"topbar_logo_slika": topbarLogoSlika, "topbar_logo_slika": topbarLogoSlika,
"topbar_logo_tekst": topbarLogoTekst,
// profil firme (Faza 0) — radio dugmad uvek šalju vrednost, pa se uredno čuvaju // profil firme (Faza 0) — radio dugmad uvek šalju vrednost, pa se uredno čuvaju
"firma_pravni_oblik": r.FormValue("firma_pravni_oblik"), "firma_pravni_oblik": r.FormValue("firma_pravni_oblik"),
"firma_pdv_obveznik": r.FormValue("firma_pdv_obveznik"), "firma_pdv_obveznik": r.FormValue("firma_pdv_obveznik"),
@@ -654,7 +647,6 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac
PIB: podesavanja["pib"], PIB: podesavanja["pib"],
LogoPutanja: podesavanja["logo_putanja"], LogoPutanja: podesavanja["logo_putanja"],
TopbarLogoSlika: podesavanja["topbar_logo_slika"] == "1", TopbarLogoSlika: podesavanja["topbar_logo_slika"] == "1",
TopbarLogoTekst: podesavanja["topbar_logo_tekst"] == "1",
FirmaPravniOblik: vrednostIliDefault(podesavanja, "firma_pravni_oblik", "pausalac"), FirmaPravniOblik: vrednostIliDefault(podesavanja, "firma_pravni_oblik", "pausalac"),
FirmaPdvObveznik: vrednostIliDefault(podesavanja, "firma_pdv_obveznik", "ne"), FirmaPdvObveznik: vrednostIliDefault(podesavanja, "firma_pdv_obveznik", "ne"),
FirmaFiskalizacija: vrednostIliDefault(podesavanja, "firma_fiskalizacija", "ne"), FirmaFiskalizacija: vrednostIliDefault(podesavanja, "firma_fiskalizacija", "ne"),
+6 -1
View File
@@ -328,7 +328,12 @@ func (h *Handler) ProfilOtpremiAvatar(w http.ResponseWriter, r *http.Request) {
} }
buf := make([]byte, 512) buf := make([]byte, 512)
n, _ := fajl.Read(buf) n, err := fajl.Read(buf)
if err != nil && err != io.EOF {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čitanju fajla.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
stvarniMime := http.DetectContentType(buf[:n]) stvarniMime := http.DetectContentType(buf[:n])
if !strings.HasPrefix(stvarniMime, ocekivaniMime) { if !strings.HasPrefix(stvarniMime, ocekivaniMime) {
middleware.SetFlash(w, r, h.DB, "greska", "Sadržaj fajla ne odgovara odabranoj ekstenziji.") middleware.SetFlash(w, r, h.DB, "greska", "Sadržaj fajla ne odgovara odabranoj ekstenziji.")
-1
View File
@@ -38,7 +38,6 @@ type PodaciStranice struct {
Podnazlov string Podnazlov string
LogoPutanja string // putanja do slike loga firme LogoPutanja string // putanja do slike loga firme
TopbarLogoSlika bool // prikaži logo sliku u topbaru TopbarLogoSlika bool // prikaži logo sliku u topbaru
TopbarLogoTekst bool // prikaži naziv firme u topbaru
AvatarPutanja string // putanja do lične avatar slike korisnika AvatarPutanja string // putanja do lične avatar slike korisnika
Korisnik string Korisnik string
KorisnikIme string // korisničko ime prijavljenog korisnika KorisnikIme string // korisničko ime prijavljenog korisnika
+23 -23
View File
@@ -9,27 +9,27 @@
<div class="poruka-uspeh">Podešavanja su uspešno sačuvana.</div> <div class="poruka-uspeh">Podešavanja su uspešno sačuvana.</div>
{{end}} {{end}}
<!-- upload loga — posebna forma jer je multipart, mora biti van glavne forme --> <!-- logo kartica: dve forme su sestre unutar div.kartica, nisu ugnježdene -->
<form method="POST" action="/podesavanja/logo" enctype="multipart/form-data"> <div class="kartica animiraj" style="margin-bottom:16px;">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}"> <div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
<div class="kartica animiraj" style="margin-bottom:16px;"> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);"> <span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Logo firme</span>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> </div>
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Logo firme</span> {{if .LogoPutanja}}
</div> <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
{{if .LogoPutanja}} <img src="{{.LogoPutanja}}" alt="Trenutni logo"
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;"> style="max-height:60px;max-width:200px;object-fit:contain;border:0.5px solid var(--ivica);border-radius:8px;padding:6px;background:var(--kartica);flex-shrink:0;">
<img src="{{.LogoPutanja}}" alt="Trenutni logo" <form method="POST" action="/podesavanja/logo/ukloni" style="margin:0;">
style="max-height:60px;max-width:200px;object-fit:contain;border:0.5px solid var(--ivica);border-radius:8px;padding:6px;background:var(--kartica);flex-shrink:0;"> <input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<form method="POST" action="/podesavanja/logo/ukloni" style="margin:0;"> <button type="submit"
<input type="hidden" name="_csrf" value="{{.CsrfToken}}"> style="padding:6px 14px;background:none;border:0.5px solid #dc2626;color:#dc2626;border-radius:8px;font-size:13px;cursor:pointer;white-space:nowrap;">
<button type="submit" Ukloni sliku
style="padding:6px 14px;background:none;border:0.5px solid #dc2626;color:#dc2626;border-radius:8px;font-size:13px;cursor:pointer;white-space:nowrap;"> </button>
Ukloni sliku </form>
</button> </div>
</form> {{end}}
</div> <form method="POST" action="/podesavanja/logo" enctype="multipart/form-data">
{{end}} <input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;"> <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
<input type="file" id="logo-file" name="logo" accept=".png,.jpg,.jpeg,.svg" <input type="file" id="logo-file" name="logo" accept=".png,.jpg,.jpeg,.svg"
style="display:none;" style="display:none;"
@@ -46,8 +46,8 @@
</button> </button>
</div> </div>
<div class="pomocni-tekst" style="font-size:12px;margin-top:6px;">PNG, JPG ili SVG — maksimum 2 MB</div> <div class="pomocni-tekst" style="font-size:12px;margin-top:6px;">PNG, JPG ili SVG — maksimum 2 MB</div>
</div> </form>
</form> </div>
<!-- firma: naziv, podnazlov, adresa, telefon, PIB, logo zona --> <!-- firma: naziv, podnazlov, adresa, telefon, PIB, logo zona -->
<form method="POST" action="/podesavanja/sacuvaj"> <form method="POST" action="/podesavanja/sacuvaj">