Bezbednosni audit i refaktoring: HP popravke, RBAC, flash poruke, go:embed, CSP

This commit is contained in:
2026-06-07 16:10:41 +02:00
parent 301bcaf5c4
commit 16b993933c
37 changed files with 1513 additions and 1949 deletions
+33 -41
View File
@@ -11,6 +11,33 @@ import (
type sqliteKorisniciRepo struct{ db *sql.DB }
// skeniraiKorisnika čita jedan red iz baze i popunjava model.Korisnik
func skeniraiKorisnika(row interface{ Scan(...any) error }) (*model.Korisnik, error) {
k := &model.Korisnik{}
var aktivan, koristiLokalnuTemu int
var lokalnaTema sql.NullString
var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity sql.NullString
var datumKreiranja time.Time
if err := row.Scan(
&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &k.TotpTajna,
&lokalnaTema, &koristiLokalnuTemu, &datumKreiranja,
&lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur,
&lokalnaPozadinaBlurPozadine, &lokalnaPozadinaGlassOpacity,
); err != nil {
return nil, err
}
k.Aktivan = aktivan == 1
k.LokalnaTema = lokalnaTema.String
k.KoristiLokalnuTemu = koristiLokalnuTemu == 1
k.DatumKreiranja = datumKreiranja
k.LokalnaPozadina = lokalnaPozadina.String
k.LokalnaPozadinaOpacity = lokalnaPozadinaOpacity.String
k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String
k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String
k.LokalnaPozadinaGlassOpacity = lokalnaPozadinaGlassOpacity.String
return k, nil
}
// NoviKorisniciRepo kreira SQLite implementaciju KorisniciRepository
func NoviKorisniciRepo(db *sql.DB) *sqliteKorisniciRepo {
return &sqliteKorisniciRepo{db: db}
@@ -28,67 +55,32 @@ func (r *sqliteKorisniciRepo) Kreiraj(ctx context.Context, korisnickoIme, lozink
}
func (r *sqliteKorisniciRepo) DohvatiPoImenu(ctx context.Context, korisnickoIme string) (*model.Korisnik, error) {
k := &model.Korisnik{}
var aktivan, koristiLokalnuTemu int
var totpTajna, lokalnaTema sql.NullString
var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity sql.NullString
var datumKreiranja time.Time
err := r.db.QueryRowContext(ctx,
row := r.db.QueryRowContext(ctx,
`SELECT id, korisnicko_ime, lozinka_hash, uloga, aktivan, COALESCE(totp_tajna, ''),
COALESCE(lokalna_tema, ''), koristi_lokalnu_temu, datum_kreiranja,
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0'),
COALESCE(lokalna_pozadina_glass_opacity, '10')
FROM korisnici WHERE korisnicko_ime = ?`, korisnickoIme).
Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &totpTajna,
&lokalnaTema, &koristiLokalnuTemu, &datumKreiranja,
&lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &lokalnaPozadinaBlurPozadine,
&lokalnaPozadinaGlassOpacity)
FROM korisnici WHERE korisnicko_ime = ?`, korisnickoIme)
k, err := skeniraiKorisnika(row)
if err != nil {
return nil, fmt.Errorf("ntech: korisnici.DohvatiPoImenu: %w", err)
}
k.Aktivan = aktivan == 1
k.TotpTajna = totpTajna.String
k.LokalnaTema = lokalnaTema.String
k.KoristiLokalnuTemu = koristiLokalnuTemu == 1
k.DatumKreiranja = datumKreiranja
k.LokalnaPozadina = lokalnaPozadina.String
k.LokalnaPozadinaOpacity = lokalnaPozadinaOpacity.String
k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String
k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String
k.LokalnaPozadinaGlassOpacity = lokalnaPozadinaGlassOpacity.String
return k, nil
}
func (r *sqliteKorisniciRepo) DohvatiPoID(ctx context.Context, id int64) (*model.Korisnik, error) {
k := &model.Korisnik{}
var aktivan, koristiLokalnuTemu int
var lokalnaTema sql.NullString
var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity sql.NullString
var datumKreiranja time.Time
err := r.db.QueryRowContext(ctx,
row := r.db.QueryRowContext(ctx,
`SELECT id, korisnicko_ime, lozinka_hash, uloga, aktivan, COALESCE(totp_tajna, ''),
COALESCE(lokalna_tema, ''), koristi_lokalnu_temu, datum_kreiranja,
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0'),
COALESCE(lokalna_pozadina_glass_opacity, '10')
FROM korisnici WHERE id = ?`, id).
Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &k.TotpTajna,
&lokalnaTema, &koristiLokalnuTemu, &datumKreiranja,
&lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &lokalnaPozadinaBlurPozadine,
&lokalnaPozadinaGlassOpacity)
FROM korisnici WHERE id = ?`, id)
k, err := skeniraiKorisnika(row)
if err != nil {
return nil, fmt.Errorf("ntech: korisnici.DohvatiPoID: %w", err)
}
k.Aktivan = aktivan == 1
k.LokalnaTema = lokalnaTema.String
k.KoristiLokalnuTemu = koristiLokalnuTemu == 1
k.DatumKreiranja = datumKreiranja
k.LokalnaPozadina = lokalnaPozadina.String
k.LokalnaPozadinaOpacity = lokalnaPozadinaOpacity.String
k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String
k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String
k.LokalnaPozadinaGlassOpacity = lokalnaPozadinaGlassOpacity.String
return k, nil
}
-38
View File
@@ -305,44 +305,6 @@ func (h *Handler) AdminProfil(w http.ResponseWriter, r *http.Request) {
})
}
// AdminSacuvajLokalnuTemu čuva korisnikovu lokalnu temu
func (h *Handler) AdminSacuvajLokalnuTemu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil {
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
return
}
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
http.Error(w, "Pristup odbijen", http.StatusForbidden)
return
}
if err := r.ParseForm(); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
return
}
koristi := r.FormValue("koristi_lokalnu_temu") == "1"
lokalnaTema := r.FormValue("lokalna_tema")
if lokalnaTema != "tamna" && lokalnaTema != "svetla" {
lokalnaTema = "tamna"
}
if err := h.KorisniciRepo.SacuvajLokalnuTemu(r.Context(), k.ID, lokalnaTema, koristi); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Tema je sačuvana.")
// vrati korisnika na stranicu odakle je došao (Referer), ili na profil kao fallback
if ref := r.Referer(); ref != "" {
http.Redirect(w, r, ref, http.StatusSeeOther)
return
}
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
}
// AdminPromeniLozinku menja lozinku prijavljenog korisnika
func (h *Handler) AdminPromeniLozinku(w http.ResponseWriter, r *http.Request) {
+1 -1
View File
@@ -70,7 +70,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
if korisnikDash.Uloga == "radnik" {
korisnikFilter.KorisnikID = &korisnikDash.ID
}
if n, err := h.PodsetniciFRepo.BrojAktivnih(ctx, korisnikFilter); err != nil {
if n, err := h.PodsetnikRepo.BrojAktivnih(ctx, korisnikFilter); err != nil {
log.Printf("dashboard: aktivni podsetnici: %v", err)
} else {
aktivniPodsetnici = n
+5 -5
View File
@@ -25,7 +25,7 @@ type Handler struct {
ProdajaRepo db.ProdajaRepository
KorisniciRepo db.KorisniciRepository
SesijeRepo db.SesijeRepository
PodsetniciFRepo db.PodsetnikRepository
PodsetnikRepo db.PodsetnikRepository
PokusajiRepo db.PokusajiPrijaveRepository
LoginIstorijsaRepo db.LoginIstorijsaRepository
DozvoleRepo db.DozvoleRepository
@@ -47,15 +47,15 @@ func Novi(baza *sql.DB) *Handler {
ProdajaRepo: sqlite.NoviProdajaRepo(baza),
KorisniciRepo: sqlite.NoviKorisniciRepo(baza),
SesijeRepo: sqlite.NoviSesijeRepo(baza),
PodsetniciFRepo: sqlite.NoviPodsetnikRepo(baza),
PodsetnikRepo: sqlite.NoviPodsetnikRepo(baza),
PokusajiRepo: sqlite.NoviPokusajiPrijaveRepo(baza),
LoginIstorijsaRepo: sqlite.NoviLoginIstorijsaRepo(baza),
DozvoleRepo: sqlite.NoviDozvoleRepo(baza, middleware.ImaDozvolu, middleware.SveAkcije()),
}
}
// reinicijalzijRepozitorijume zamenjuje sve repozitorijume posle obnove baze
func (h *Handler) reinicijalzijRepozitorijume(novaDB *sql.DB) {
// reinicijalizujRepozitorijume zamenjuje sve repozitorijume posle obnove baze
func (h *Handler) reinicijalizujRepozitorijume(novaDB *sql.DB) {
h.DB = novaDB
h.Artikli = sqlite.NoviArtikalRepo(novaDB)
h.KategorijeRepo = sqlite.NovaKategorijaRepo(novaDB)
@@ -66,7 +66,7 @@ func (h *Handler) reinicijalzijRepozitorijume(novaDB *sql.DB) {
h.ProdajaRepo = sqlite.NoviProdajaRepo(novaDB)
h.KorisniciRepo = sqlite.NoviKorisniciRepo(novaDB)
h.SesijeRepo = sqlite.NoviSesijeRepo(novaDB)
h.PodsetniciFRepo = sqlite.NoviPodsetnikRepo(novaDB)
h.PodsetnikRepo = sqlite.NoviPodsetnikRepo(novaDB)
h.PokusajiRepo = sqlite.NoviPokusajiPrijaveRepo(novaDB)
h.LoginIstorijsaRepo = sqlite.NoviLoginIstorijsaRepo(novaDB)
h.DozvoleRepo = sqlite.NoviDozvoleRepo(novaDB, middleware.ImaDozvolu, middleware.SveAkcije())
+19 -47
View File
@@ -82,27 +82,11 @@ func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) {
Verzija: h.Verzija,
LogoGreska: r.URL.Query().Get("logo_greska"),
Backupi: ucitajListuBackupa(),
LoginPozadina: podesavanja["login_pozadina"],
LoginPozadinaOpacity: func() string {
v := podesavanja["login_pozadina_opacity"]
if v == "" { return "50" }
return v
}(),
LoginPozadinaBlurPozadine: func() string {
v := podesavanja["login_pozadina_blur_pozadine"]
if v == "" { return "0" }
return v
}(),
LoginPozadinaBlurKartice: func() string {
v := podesavanja["login_pozadina_blur_kartice"]
if v == "" { return "12" }
return v
}(),
LoginPozadinaZatamnjenjeKartice: func() string {
v := podesavanja["login_pozadina_zatamnjenje_kartice"]
if v == "" { return "0" }
return v
}(),
LoginPozadina: podesavanja["login_pozadina"],
LoginPozadinaOpacity: vrednostIliDefault(podesavanja, "login_pozadina_opacity", "50"),
LoginPozadinaBlurPozadine: vrednostIliDefault(podesavanja, "login_pozadina_blur_pozadine", "0"),
LoginPozadinaBlurKartice: vrednostIliDefault(podesavanja, "login_pozadina_blur_kartice", "12"),
LoginPozadinaZatamnjenjeKartice: vrednostIliDefault(podesavanja, "login_pozadina_zatamnjenje_kartice", "0"),
}
h.renderujTemplate(w, "podesavanja", podaci)
@@ -136,6 +120,14 @@ func ucitajListuBackupa() []BackupInfo {
return lista
}
// vrednostIliDefault vraća vrednost iz mape ako postoji i nije prazan string, inače vraća podrazumevanu vrednost
func vrednostIliDefault(m map[string]string, kljuc, podrazumevano string) string {
if v := m[kljuc]; v != "" {
return v
}
return podrazumevano
}
// VratiBackup zamenjuje trenutnu bazu sa izabranim backup fajlom
func (h *Handler) VratiBackup(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
@@ -197,7 +189,7 @@ func (h *Handler) VratiBackup(w http.ResponseWriter, r *http.Request) {
return
}
h.reinicijalzijRepozitorijume(novaDB)
h.reinicijalizujRepozitorijume(novaDB)
log.Printf("Baza uspešno obnovljena iz: %s", ime)
http.Redirect(w, r, "/podesavanja?sacuvano=vraceno", http.StatusSeeOther)
@@ -594,31 +586,11 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac
Verzija: h.Verzija,
LogoGreska: r.URL.Query().Get("logo_greska"),
Backupi: ucitajListuBackupa(),
LoginPozadina: podesavanja["login_pozadina"],
LoginPozadinaOpacity: func() string {
if v := podesavanja["login_pozadina_opacity"]; v != "" {
return v
}
return "50"
}(),
LoginPozadinaBlurPozadine: func() string {
if v := podesavanja["login_pozadina_blur_pozadine"]; v != "" {
return v
}
return "0"
}(),
LoginPozadinaBlurKartice: func() string {
if v := podesavanja["login_pozadina_blur_kartice"]; v != "" {
return v
}
return "12"
}(),
LoginPozadinaZatamnjenjeKartice: func() string {
if v := podesavanja["login_pozadina_zatamnjenje_kartice"]; v != "" {
return v
}
return "0"
}(),
LoginPozadina: podesavanja["login_pozadina"],
LoginPozadinaOpacity: vrednostIliDefault(podesavanja, "login_pozadina_opacity", "50"),
LoginPozadinaBlurPozadine: vrednostIliDefault(podesavanja, "login_pozadina_blur_pozadine", "0"),
LoginPozadinaBlurKartice: vrednostIliDefault(podesavanja, "login_pozadina_blur_kartice", "12"),
LoginPozadinaZatamnjenjeKartice: vrednostIliDefault(podesavanja, "login_pozadina_zatamnjenje_kartice", "0"),
}, nil
}
+37 -50
View File
@@ -52,7 +52,7 @@ func (h *Handler) Podsetnici(w http.ResponseWriter, r *http.Request) {
filter.KorisnikID = &k.ID
}
lista, err := h.PodsetniciFRepo.Lista(r.Context(), filter)
lista, err := h.PodsetnikRepo.Lista(r.Context(), filter)
if err != nil {
http.Error(w, "Greška pri učitavanju podsetnika", http.StatusInternalServerError)
return
@@ -88,7 +88,7 @@ func (h *Handler) NoviPodsetnik(w http.ResponseWriter, r *http.Request) {
ps.NaslovStranice = "Novi podsetnik"
var korisnici []model.Korisnik
if k.Uloga == "admin" || k.Uloga == "superadmin" {
if middleware.JeAdmin(k) {
korisnici, _ = h.KorisniciRepo.Lista(r.Context())
}
@@ -110,32 +110,14 @@ func (h *Handler) SacuvajPodsetnik(w http.ResponseWriter, r *http.Request) {
podsetnik, greska := parseFormuPodsetnika(r, k)
prikaziGresku := func(poruka string) {
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "podsetnici"
ps.NaslovStranice = "Novi podsetnik"
var korisnici []model.Korisnik
if k.Uloga == "admin" || k.Uloga == "superadmin" {
korisnici, _ = h.KorisniciRepo.Lista(r.Context())
}
h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
PodaciStranice: ps,
Podsetnik: podsetnik,
Greska: poruka,
Izmena: false,
Korisnici: korisnici,
})
}
if greska != "" {
prikaziGresku(greska)
h.prikaziGreskuPodsetnika(w, r, k, podsetnik, greska, false)
return
}
if _, err := h.PodsetniciFRepo.Kreiraj(r.Context(), &podsetnik); err != nil {
if _, err := h.PodsetnikRepo.Kreiraj(r.Context(), &podsetnik); err != nil {
log.Printf("greška pri čuvanju podsetnika: %v", err)
prikaziGresku("Došlo je do greške pri čuvanju. Pokušajte ponovo.")
h.prikaziGreskuPodsetnika(w, r, k, podsetnik, "Došlo je do greške pri čuvanju. Pokušajte ponovo.", false)
return
}
@@ -152,7 +134,7 @@ func (h *Handler) IzmeniPodsetnik(w http.ResponseWriter, r *http.Request) {
return
}
podsetnik, err := h.PodsetniciFRepo.DohvatiID(r.Context(), id)
podsetnik, err := h.PodsetnikRepo.DohvatiID(r.Context(), id)
if err != nil {
http.Error(w, "Podsetnik nije pronađen", http.StatusNotFound)
return
@@ -169,7 +151,7 @@ func (h *Handler) IzmeniPodsetnik(w http.ResponseWriter, r *http.Request) {
ps.NaslovStranice = "Izmeni podsetnik"
var korisnici []model.Korisnik
if k.Uloga == "admin" || k.Uloga == "superadmin" {
if middleware.JeAdmin(k) {
korisnici, _ = h.KorisniciRepo.Lista(r.Context())
}
@@ -199,32 +181,14 @@ func (h *Handler) SacuvajIzmenePodsetnika(w http.ResponseWriter, r *http.Request
podsetnik, greska := parseFormuPodsetnika(r, k)
podsetnik.ID = id
prikaziGresku := func(poruka string) {
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "podsetnici"
ps.NaslovStranice = "Izmeni podsetnik"
var korisnici []model.Korisnik
if k.Uloga == "admin" || k.Uloga == "superadmin" {
korisnici, _ = h.KorisniciRepo.Lista(r.Context())
}
h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
PodaciStranice: ps,
Podsetnik: podsetnik,
Greska: poruka,
Izmena: true,
Korisnici: korisnici,
})
}
if greska != "" {
prikaziGresku(greska)
h.prikaziGreskuPodsetnika(w, r, k, podsetnik, greska, true)
return
}
if err := h.PodsetniciFRepo.Izmeni(r.Context(), &podsetnik); err != nil {
if err := h.PodsetnikRepo.Izmeni(r.Context(), &podsetnik); err != nil {
log.Printf("greška pri čuvanju izmene podsetnika: %v", err)
prikaziGresku("Došlo je do greške pri čuvanju. Pokušajte ponovo.")
h.prikaziGreskuPodsetnika(w, r, k, podsetnik, "Došlo je do greške pri čuvanju. Pokušajte ponovo.", true)
return
}
@@ -240,13 +204,13 @@ func (h *Handler) OznaciPodsetnik(w http.ResponseWriter, r *http.Request) {
}
// učitamo trenutni status pa ga preokrenemo
podsetnik, err := h.PodsetniciFRepo.DohvatiID(r.Context(), id)
podsetnik, err := h.PodsetnikRepo.DohvatiID(r.Context(), id)
if err != nil {
http.Error(w, "Podsetnik nije pronađen", http.StatusNotFound)
return
}
if err := h.PodsetniciFRepo.OznaciZavrsenim(r.Context(), id, !podsetnik.Zavrseno); err != nil {
if err := h.PodsetnikRepo.OznaciZavrsenim(r.Context(), id, !podsetnik.Zavrseno); err != nil {
http.Error(w, "Greška pri ažuriranju statusa", http.StatusInternalServerError)
return
}
@@ -262,7 +226,7 @@ func (h *Handler) ObrisiPodsetnik(w http.ResponseWriter, r *http.Request) {
return
}
if err := h.PodsetniciFRepo.Obrisi(r.Context(), id); err != nil {
if err := h.PodsetnikRepo.Obrisi(r.Context(), id); err != nil {
http.Error(w, "Greška pri brisanju podsetnika", http.StatusInternalServerError)
return
}
@@ -295,7 +259,7 @@ func parseFormuPodsetnika(r *http.Request, k *model.Korisnik) (model.Podsetnik,
}
// admin/superadmin mogu dodeliti podsetnik drugom korisniku
if k.Uloga == "admin" || k.Uloga == "superadmin" {
if middleware.JeAdmin(k) {
if kidStr := strings.TrimSpace(r.FormValue("korisnik_id")); kidStr != "" {
if kid, err := strconv.ParseInt(kidStr, 10, 64); err == nil && kid > 0 {
p.KorisnikID = &kid
@@ -309,6 +273,29 @@ func parseFormuPodsetnika(r *http.Request, k *model.Korisnik) (model.Podsetnik,
return p, ""
}
// prikaziGreskuPodsetnika prikazuje formu podsetnika sa porukom o grešci
func (h *Handler) prikaziGreskuPodsetnika(w http.ResponseWriter, r *http.Request, k *model.Korisnik, podsetnik model.Podsetnik, poruka string, izmena bool) {
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "podsetnici"
if izmena {
ps.NaslovStranice = "Izmeni podsetnik"
} else {
ps.NaslovStranice = "Novi podsetnik"
}
var korisnici []model.Korisnik
if middleware.JeAdmin(k) {
korisnici, _ = h.KorisniciRepo.Lista(r.Context())
}
h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
PodaciStranice: ps,
Podsetnik: podsetnik,
Greska: poruka,
Izmena: izmena,
Korisnici: korisnici,
})
}
// renderujFormuPodsetnika renderuje HTML šablon forme za unos ili izmenu podsetnika
func (h *Handler) renderujFormuPodsetnika(w http.ResponseWriter, podaci podaciPodsetnikForma) {
h.renderujTemplate(w, "podsetnik_forma", podaci)
+39
View File
@@ -250,3 +250,42 @@ func (h *Handler) ProfilSacuvajPozadinuStilove(w http.ResponseWriter, r *http.Re
middleware.SetFlash(w, r, h.DB, "uspeh", "Podešavanja su sačuvana.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
}
// SacuvajLokalnuTemu čuva korisnikovu lokalnu temu
func (h *Handler) SacuvajLokalnuTemu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil {
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
return
}
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
http.Error(w, "Pristup odbijen", http.StatusForbidden)
return
}
if err := r.ParseForm(); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
return
}
koristi := r.FormValue("koristi_lokalnu_temu") == "1"
lokalnaTema := r.FormValue("lokalna_tema")
if lokalnaTema != "tamna" && lokalnaTema != "svetla" {
lokalnaTema = "tamna"
}
if err := h.KorisniciRepo.SacuvajLokalnuTemu(r.Context(), k.ID, lokalnaTema, koristi); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Tema je sačuvana.")
// vrati korisnika na stranicu odakle je došao (Referer), ili na profil kao fallback
if ref := r.Referer(); ref != "" {
http.Redirect(w, r, ref, http.StatusSeeOther)
return
}
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
}
+2 -1
View File
@@ -14,7 +14,8 @@ func BezbednostHeaders() func(http.Handler) http.Handler {
h.Set("Content-Security-Policy",
"default-src 'self'; "+
"style-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com; "+
"script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com https://cdn.jsdelivr.net; "+
// Alpine.js v3 koristi new Function() interno — unsafe-eval je neophodan; CSP build zahteva značajan refaktoring.
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.tailwindcss.com https://cdn.jsdelivr.net; "+
"img-src 'self' data: blob:; "+
"font-src 'self'; "+
"connect-src 'self'")