904 lines
31 KiB
Go
904 lines
31 KiB
Go
package handler
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
ntechsqlite "ntech/internal/db/sqlite"
|
|
"ntech/internal/middleware"
|
|
"ntech/internal/model"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// PodaciPodesavanja su podaci za stranicu podešavanja
|
|
type PodaciPodesavanja struct {
|
|
model.PodaciStranice
|
|
NazivFirme string
|
|
Podnazlov string
|
|
Adresa string
|
|
Telefon string
|
|
PIB string
|
|
LogoTip string
|
|
LogoPutanja string
|
|
Tema string
|
|
Sacuvano bool
|
|
Verzija string
|
|
LogoGreska string
|
|
BackupVracen bool
|
|
Backupi []BackupInfo
|
|
LoginPozadina string
|
|
LoginPozadinaOpacity string
|
|
LoginPozadinaBlurPozadine string
|
|
LoginPozadinaBlurKartice string
|
|
AppPozadina string
|
|
AppPozadinaOpacity string
|
|
AppPozadinaBlur string
|
|
AppPozadinaBlurPozadine string
|
|
}
|
|
|
|
// BackupInfo opisuje jedan backup fajl
|
|
type BackupInfo struct {
|
|
Ime string
|
|
Datum string
|
|
Velicina string
|
|
}
|
|
|
|
// validnoImeBackupa proverava da li je ime backup fajla bezbedno (bez path traversala)
|
|
var validnoImeBackupa = regexp.MustCompile(`^ntech_\d{8}_\d{6}\.db$`)
|
|
|
|
// Podesavanja renderuje stranicu podešavanja
|
|
func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) {
|
|
k := middleware.KorisnikIzKonteksta(r.Context())
|
|
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.pregled") {
|
|
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
|
return
|
|
}
|
|
podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
|
if err != nil {
|
|
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
ps := h.popuniPodaciStranice(r, podesavanja)
|
|
ps.Stranica = "podesavanja"
|
|
ps.NaslovStranice = "Podešavanja"
|
|
podaci := PodaciPodesavanja{
|
|
PodaciStranice: ps,
|
|
NazivFirme: podesavanja["naziv_firme"],
|
|
Podnazlov: podesavanja["podnazlov"],
|
|
Adresa: podesavanja["adresa"],
|
|
Telefon: podesavanja["telefon"],
|
|
PIB: podesavanja["pib"],
|
|
LogoTip: podesavanja["logo_tip"],
|
|
LogoPutanja: podesavanja["logo_putanja"],
|
|
Tema: podesavanja["tema"],
|
|
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
|
BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno",
|
|
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
|
|
}(),
|
|
AppPozadina: podesavanja["app_pozadina"],
|
|
AppPozadinaOpacity: func() string {
|
|
v := podesavanja["app_pozadina_opacity"]
|
|
if v == "" { return "50" }
|
|
return v
|
|
}(),
|
|
AppPozadinaBlur: func() string {
|
|
v := podesavanja["app_pozadina_blur"]
|
|
if v == "" { return "12" }
|
|
return v
|
|
}(),
|
|
AppPozadinaBlurPozadine: func() string {
|
|
v := podesavanja["app_pozadina_blur_pozadine"]
|
|
if v == "" { return "0" }
|
|
return v
|
|
}(),
|
|
}
|
|
|
|
h.renderujTemplate(w, "podesavanja", podaci)
|
|
}
|
|
|
|
// ucitajListuBackupa vraća sortiranu listu fajlova iz backups/ foldera
|
|
func ucitajListuBackupa() []BackupInfo {
|
|
fajlovi, _ := filepath.Glob(filepath.Join("backups", "ntech_*.db"))
|
|
sort.Sort(sort.Reverse(sort.StringSlice(fajlovi)))
|
|
var lista []BackupInfo
|
|
for _, f := range fajlovi {
|
|
info, err := os.Stat(f)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
vel := info.Size()
|
|
var velStr string
|
|
switch {
|
|
case vel >= 1024*1024:
|
|
velStr = fmt.Sprintf("%.1f MB", float64(vel)/(1024*1024))
|
|
default:
|
|
velStr = fmt.Sprintf("%d KB", vel/1024)
|
|
}
|
|
datum := info.ModTime().Format("02.01.2006. 15:04:05")
|
|
lista = append(lista, BackupInfo{
|
|
Ime: filepath.Base(f),
|
|
Datum: datum,
|
|
Velicina: velStr,
|
|
})
|
|
}
|
|
return lista
|
|
}
|
|
|
|
// VratiBackup zamenjuje trenutnu bazu sa izabranim backup fajlom
|
|
func (h *Handler) VratiBackup(w http.ResponseWriter, r *http.Request) {
|
|
k := middleware.KorisnikIzKonteksta(r.Context())
|
|
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "backup.pokreni") {
|
|
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
|
return
|
|
}
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Redirect(w, r, "/podesavanja?backup_greska=Greška+pri+čitanju+zahteva", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
ime := r.FormValue("ime")
|
|
if !validnoImeBackupa.MatchString(ime) {
|
|
http.Redirect(w, r, "/podesavanja?backup_greska=Neispravan+naziv+fajla", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
putanjaBackupa := filepath.Join("backups", ime)
|
|
if _, err := os.Stat(putanjaBackupa); err != nil {
|
|
http.Redirect(w, r, "/podesavanja?backup_greska=Backup+fajl+nije+pronađen", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// pre obnove, sačuvaj trenutno stanje baze
|
|
sigurnosni := filepath.Join("backups", fmt.Sprintf("ntech_%s_pred_vracanjem.db", time.Now().Format("20060102_150405")))
|
|
if _, err := h.DB.ExecContext(r.Context(), "VACUUM INTO ?", sigurnosni); err != nil {
|
|
log.Printf("vrati backup: greška pri kreiranju sigurnosne kopije: %v", err)
|
|
http.Redirect(w, r, "/podesavanja?backup_greska=Greška+pri+kreiranju+sigurnosne+kopije", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// isprazni WAL u glavni fajl
|
|
if _, err := h.DB.ExecContext(r.Context(), "PRAGMA wal_checkpoint(TRUNCATE)"); err != nil {
|
|
log.Printf("vrati backup: wal_checkpoint greška: %v", err)
|
|
}
|
|
|
|
// zatvori sve konekcije
|
|
if err := h.DB.Close(); err != nil {
|
|
log.Printf("vrati backup: greška pri zatvaranju baze: %v", err)
|
|
}
|
|
|
|
// kopiraj backup fajl na mesto trenutne baze
|
|
if err := kopiraFajl(putanjaBackupa, h.PutanjaBaze); err != nil {
|
|
log.Printf("vrati backup: greška pri kopiranju: %v", err)
|
|
http.Redirect(w, r, "/podesavanja?backup_greska=Greška+pri+obnovi+baze", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// ukloni WAL i SHM fajlove stare baze
|
|
os.Remove(h.PutanjaBaze + "-wal")
|
|
os.Remove(h.PutanjaBaze + "-shm")
|
|
|
|
// otvori novu konekciju
|
|
novaDB, err := ntechsqlite.OtvoriDB(h.PutanjaBaze)
|
|
if err != nil {
|
|
log.Printf("vrati backup: greška pri otvaranju nove baze: %v", err)
|
|
http.Redirect(w, r, "/podesavanja?backup_greska=Baza+obnovljena+ali+je+potreban+restart", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
h.reinicijalzijRepozitorijume(novaDB)
|
|
log.Printf("Baza uspešno obnovljena iz: %s", ime)
|
|
|
|
http.Redirect(w, r, "/podesavanja?sacuvano=vraceno", http.StatusSeeOther)
|
|
}
|
|
|
|
// kopiraFajl kopira fajl sa izvora na odredište
|
|
func kopiraFajl(izvor, odrediste string) error {
|
|
src, err := os.Open(izvor)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer src.Close()
|
|
|
|
dst, err := os.Create(odrediste)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer dst.Close()
|
|
|
|
_, err = io.Copy(dst, src)
|
|
return err
|
|
}
|
|
|
|
// SacuvajPodesavanja prima POST i čuva podešavanja u bazu
|
|
func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
|
|
k := middleware.KorisnikIzKonteksta(r.Context())
|
|
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.izmeni") {
|
|
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
|
return
|
|
}
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
polja := map[string]string{
|
|
"naziv_firme": r.FormValue("naziv_firme"),
|
|
"podnazlov": r.FormValue("podnazlov"),
|
|
"adresa": r.FormValue("adresa"),
|
|
"telefon": r.FormValue("telefon"),
|
|
"pib": r.FormValue("pib"),
|
|
"logo_tip": r.FormValue("logo_tip"),
|
|
"tema": r.FormValue("tema"),
|
|
}
|
|
|
|
for kljuc, vrednost := range polja {
|
|
if vrednost == "" {
|
|
continue
|
|
}
|
|
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, kljuc, vrednost); err != nil {
|
|
http.Error(w, "Greška pri čuvanju podešavanja", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
sledeci := r.FormValue("_next")
|
|
if sledeci == "" {
|
|
http.Redirect(w, r, "/podesavanja?sacuvano=1", http.StatusSeeOther)
|
|
} else {
|
|
http.Redirect(w, r, sledeci+"?sacuvano=1", http.StatusSeeOther)
|
|
}
|
|
}
|
|
|
|
// BackupBaze kreira konzistentnu kopiju baze i šalje je kao attachment
|
|
func (h *Handler) BackupBaze(w http.ResponseWriter, r *http.Request) {
|
|
k := middleware.KorisnikIzKonteksta(r.Context())
|
|
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "backup.pregled") {
|
|
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
|
return
|
|
}
|
|
privremeni := fmt.Sprintf("%s/ntech_backup_%s.db", os.TempDir(), time.Now().Format("20060102_150405"))
|
|
|
|
if _, err := h.DB.ExecContext(r.Context(), "VACUUM INTO ?", privremeni); err != nil {
|
|
http.Error(w, "Greška pri kreiranju rezervne kopije", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer os.Remove(privremeni)
|
|
|
|
ime := fmt.Sprintf("ntech_backup_%s.db", time.Now().Format("20060102"))
|
|
w.Header().Set("Content-Disposition", "attachment; filename=\""+ime+"\"")
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
http.ServeFile(w, r, privremeni)
|
|
}
|
|
|
|
// OtpremiLogo prima multipart upload slike loga i čuva je u web/static/uploads/
|
|
func (h *Handler) OtpremiLogo(w http.ResponseWriter, r *http.Request) {
|
|
k := middleware.KorisnikIzKonteksta(r.Context())
|
|
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.izmeni") {
|
|
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
|
return
|
|
}
|
|
// ograničavamo telo zahteva na 2MB + malo za zaglavlja forme
|
|
r.Body = http.MaxBytesReader(w, r.Body, 2<<20+4096)
|
|
if err := r.ParseMultipartForm(2 << 20); err != nil {
|
|
http.Redirect(w, r, "/podesavanja?logo_greska=Fajl+je+prevelik+%28maksimum+2+MB%29", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
fajl, zaglavlje, err := r.FormFile("logo")
|
|
if err != nil {
|
|
http.Redirect(w, r, "/podesavanja?logo_greska=Nije+odabran+fajl", http.StatusSeeOther)
|
|
return
|
|
}
|
|
defer fajl.Close()
|
|
|
|
// eksplicitna provera veličine (zaglavlje.Size je postavljeno od strane browsera)
|
|
if zaglavlje.Size > 2<<20 {
|
|
http.Redirect(w, r, "/podesavanja?logo_greska=Fajl+je+prevelik+%28maksimum+2+MB%29", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// proveravamo ekstenziju
|
|
ext := strings.ToLower(filepath.Ext(zaglavlje.Filename))
|
|
dozvoljenoExt := map[string]string{
|
|
".png": "image/png",
|
|
".jpg": "image/jpeg",
|
|
".jpeg": "image/jpeg",
|
|
".svg": "image/svg+xml",
|
|
}
|
|
ocekivaniMime, ok := dozvoljenoExt[ext]
|
|
if !ok {
|
|
http.Redirect(w, r, "/podesavanja?logo_greska=Dozvoljeni+formati+su+PNG%2C+JPG+i+SVG", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// za binarne formate proveravamo i stvarni tip fajla (SVG je tekstualni, preskačemo)
|
|
if ext != ".svg" {
|
|
buf := make([]byte, 512)
|
|
n, _ := fajl.Read(buf)
|
|
stvarniMime := http.DetectContentType(buf[:n])
|
|
if !strings.HasPrefix(stvarniMime, ocekivaniMime) {
|
|
http.Redirect(w, r, "/podesavanja?logo_greska=Sadržaj+fajla+ne+odgovara+odabranoj+ekstenziji", http.StatusSeeOther)
|
|
return
|
|
}
|
|
// vraćamo kursor na početak
|
|
if _, err := fajl.Seek(0, io.SeekStart); err != nil {
|
|
http.Redirect(w, r, "/podesavanja?logo_greska=Greška+pri+obradi+fajla", http.StatusSeeOther)
|
|
return
|
|
}
|
|
}
|
|
|
|
// brišemo stare logo fajlove
|
|
stari, _ := filepath.Glob("web/static/uploads/logo.*")
|
|
for _, s := range stari {
|
|
os.Remove(s)
|
|
}
|
|
|
|
odrediste := "web/static/uploads/logo" + ext
|
|
dst, err := os.Create(odrediste)
|
|
if err != nil {
|
|
log.Printf("upload loga: ne mogu kreirati fajl: %v", err)
|
|
http.Redirect(w, r, "/podesavanja?logo_greska=Greška+pri+čuvanju+fajla", http.StatusSeeOther)
|
|
return
|
|
}
|
|
defer dst.Close()
|
|
|
|
if _, err := io.Copy(dst, fajl); err != nil {
|
|
log.Printf("upload loga: greška pri kopiranju: %v", err)
|
|
http.Redirect(w, r, "/podesavanja?logo_greska=Greška+pri+čuvanju+fajla", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// timestamp u URL-u sprečava browser da koristi staru keširanu sliku
|
|
putanja := fmt.Sprintf("/static/uploads/logo%s?v=%d", ext, time.Now().Unix())
|
|
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "logo_putanja", putanja); err != nil {
|
|
log.Printf("upload loga: greška pri čuvanju putanje: %v", err)
|
|
http.Redirect(w, r, "/podesavanja?logo_greska=Greška+pri+čuvanju+podešavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, "/podesavanja?sacuvano=1", http.StatusSeeOther)
|
|
}
|
|
|
|
// generisiImeUploada vraća slučajno hex ime (16 bajtova) sa datom ekstenzijom
|
|
func generisiImeUploada(ext string) (string, error) {
|
|
buf := make([]byte, 16)
|
|
if _, err := rand.Read(buf); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(buf) + ext, nil
|
|
}
|
|
|
|
// OtpremiLoginPozadinu prima multipart upload slike i čuva je kao pozadinsku sliku login stranice
|
|
func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
|
|
k := middleware.KorisnikIzKonteksta(r.Context())
|
|
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.login_pozadina") {
|
|
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
r.Body = http.MaxBytesReader(w, r.Body, 5<<20+4096)
|
|
if err := r.ParseMultipartForm(5 << 20); err != nil {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
fajl, zaglavlje, err := r.FormFile("login_pozadina")
|
|
if err != nil {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Nije odabran fajl.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
defer fajl.Close()
|
|
|
|
if zaglavlje.Size > 5<<20 {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
ext := strings.ToLower(filepath.Ext(zaglavlje.Filename))
|
|
dozvoljenoExt := map[string]string{
|
|
".jpg": "image/jpeg",
|
|
".jpeg": "image/jpeg",
|
|
".png": "image/png",
|
|
".webp": "image/webp",
|
|
}
|
|
ocekivaniMime, ok := dozvoljenoExt[ext]
|
|
if !ok {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Dozvoljeni formati su JPG, PNG i WebP.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// proveravamo stvarni tip sadržaja (magic bytes)
|
|
buf := make([]byte, 512)
|
|
n, _ := fajl.Read(buf)
|
|
stvarniMime := http.DetectContentType(buf[:n])
|
|
if !strings.HasPrefix(stvarniMime, ocekivaniMime) {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Sadržaj fajla ne odgovara odabranoj ekstenziji.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
if _, err := fajl.Seek(0, io.SeekStart); err != nil {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri obradi fajla.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// briše staru pozadinu sa diska ako postoji
|
|
staraPodesavanja, _ := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
|
if stara := staraPodesavanja["login_pozadina"]; stara != "" {
|
|
// putanja u bazi je oblika /static/uploads/ime.ext?v=..., izvlačimo samo ime fajla
|
|
deoBezverzije := strings.Split(stara, "?")[0]
|
|
staroIme := filepath.Base(deoBezverzije)
|
|
os.Remove(filepath.Join("web/static/uploads", staroIme))
|
|
}
|
|
|
|
novoIme, err := generisiImeUploada(ext)
|
|
if err != nil {
|
|
log.Printf("upload login pozadine: greška pri generisanju imena: %v", err)
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
odrediste := filepath.Join("web/static/uploads", novoIme)
|
|
dst, err := os.Create(odrediste)
|
|
if err != nil {
|
|
log.Printf("upload login pozadine: ne mogu kreirati fajl: %v", err)
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
defer dst.Close()
|
|
|
|
if _, err := io.Copy(dst, fajl); err != nil {
|
|
log.Printf("upload login pozadine: greška pri kopiranju: %v", err)
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
putanja := fmt.Sprintf("/static/uploads/%s?v=%d", novoIme, time.Now().Unix())
|
|
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "login_pozadina", putanja); err != nil {
|
|
log.Printf("upload login pozadine: greška pri čuvanju putanje: %v", err)
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika je uspešno otpremljena.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
}
|
|
|
|
// UkloniLoginPozadinu briše pozadinsku sliku login stranice sa diska i iz podešavanja
|
|
func (h *Handler) UkloniLoginPozadinu(w http.ResponseWriter, r *http.Request) {
|
|
k := middleware.KorisnikIzKonteksta(r.Context())
|
|
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.login_pozadina") {
|
|
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
|
if err == nil {
|
|
if stara := podesavanja["login_pozadina"]; stara != "" {
|
|
deoBezverzije := strings.Split(stara, "?")[0]
|
|
staroIme := filepath.Base(deoBezverzije)
|
|
os.Remove(filepath.Join("web/static/uploads", staroIme))
|
|
}
|
|
}
|
|
|
|
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "login_pozadina", ""); err != nil {
|
|
log.Printf("ukloni login pozadinu: greška pri čuvanju: %v", err)
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju slike.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika je uklonjena.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
}
|
|
|
|
// OtpremiAppPozadinu prima multipart upload slike i čuva je kao pozadinsku sliku aplikacije
|
|
func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
|
|
k := middleware.KorisnikIzKonteksta(r.Context())
|
|
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.app_pozadina") {
|
|
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
r.Body = http.MaxBytesReader(w, r.Body, 5<<20+4096)
|
|
if err := r.ParseMultipartForm(5 << 20); err != nil {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
fajl, zaglavlje, err := r.FormFile("app_pozadina")
|
|
if err != nil {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Nije odabran fajl.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
defer fajl.Close()
|
|
|
|
if zaglavlje.Size > 5<<20 {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
ext := strings.ToLower(filepath.Ext(zaglavlje.Filename))
|
|
dozvoljenoExt := map[string]string{
|
|
".jpg": "image/jpeg",
|
|
".jpeg": "image/jpeg",
|
|
".png": "image/png",
|
|
".webp": "image/webp",
|
|
}
|
|
ocekivaniMime, ok := dozvoljenoExt[ext]
|
|
if !ok {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Dozvoljeni formati su JPG, PNG i WebP.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
buf := make([]byte, 512)
|
|
n, _ := fajl.Read(buf)
|
|
stvarniMime := http.DetectContentType(buf[:n])
|
|
if !strings.HasPrefix(stvarniMime, ocekivaniMime) {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Sadržaj fajla ne odgovara odabranoj ekstenziji.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
if _, err := fajl.Seek(0, io.SeekStart); err != nil {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri obradi fajla.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// briše staru app pozadinu sa diska ako postoji
|
|
staraPodesavanja, _ := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
|
if stara := staraPodesavanja["app_pozadina"]; stara != "" {
|
|
deoBezverzije := strings.Split(stara, "?")[0]
|
|
staroIme := filepath.Base(deoBezverzije)
|
|
os.Remove(filepath.Join("web/static/uploads", staroIme))
|
|
}
|
|
|
|
novoIme, err := generisiImeUploada(ext)
|
|
if err != nil {
|
|
log.Printf("upload app pozadine: greška pri generisanju imena: %v", err)
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
odrediste := filepath.Join("web/static/uploads", novoIme)
|
|
dst, err := os.Create(odrediste)
|
|
if err != nil {
|
|
log.Printf("upload app pozadine: ne mogu kreirati fajl: %v", err)
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
defer dst.Close()
|
|
|
|
if _, err := io.Copy(dst, fajl); err != nil {
|
|
log.Printf("upload app pozadine: greška pri kopiranju: %v", err)
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
putanja := fmt.Sprintf("/static/uploads/%s?v=%d", novoIme, time.Now().Unix())
|
|
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "app_pozadina", putanja); err != nil {
|
|
log.Printf("upload app pozadine: greška pri čuvanju putanje: %v", err)
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika aplikacije je uspešno otpremljena.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
}
|
|
|
|
// UkloniAppPozadinu briše pozadinsku sliku aplikacije sa diska i iz podešavanja
|
|
func (h *Handler) UkloniAppPozadinu(w http.ResponseWriter, r *http.Request) {
|
|
k := middleware.KorisnikIzKonteksta(r.Context())
|
|
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.app_pozadina") {
|
|
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
|
if err == nil {
|
|
if stara := podesavanja["app_pozadina"]; stara != "" {
|
|
deoBezverzije := strings.Split(stara, "?")[0]
|
|
staroIme := filepath.Base(deoBezverzije)
|
|
os.Remove(filepath.Join("web/static/uploads", staroIme))
|
|
}
|
|
}
|
|
|
|
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "app_pozadina", ""); err != nil {
|
|
log.Printf("ukloni app pozadinu: greška pri čuvanju: %v", err)
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju slike.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika aplikacije je uklonjena.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
}
|
|
|
|
// SacuvajAppPozadinaStilove čuva vrednosti zamućenja i prozirnosti pozadine aplikacije
|
|
func (h *Handler) SacuvajAppPozadinaStilove(w http.ResponseWriter, r *http.Request) {
|
|
k := middleware.KorisnikIzKonteksta(r.Context())
|
|
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.app_pozadina") {
|
|
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
|
return
|
|
}
|
|
if err := r.ParseForm(); err != nil {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čitanju forme.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
blurStr := r.FormValue("blur")
|
|
blurPozadineStr := r.FormValue("blur_pozadine")
|
|
opacityStr := r.FormValue("opacity")
|
|
|
|
blurVal, err := strconv.Atoi(blurStr)
|
|
if err != nil || blurVal < 0 || blurVal > 20 {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost zamućenja stakla.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
blurPozadineVal, err := strconv.Atoi(blurPozadineStr)
|
|
if err != nil || blurPozadineVal < 0 || blurPozadineVal > 20 {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost zamućenja pozadine.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
opacityVal, err := strconv.Atoi(opacityStr)
|
|
if err != nil || opacityVal < 0 || opacityVal > 80 {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost prozirnosti.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
for kljuc, vrednost := range map[string]string{
|
|
"app_pozadina_blur": blurStr,
|
|
"app_pozadina_blur_pozadine": blurPozadineStr,
|
|
"app_pozadina_opacity": opacityStr,
|
|
} {
|
|
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, kljuc, vrednost); err != nil {
|
|
log.Printf("stilovi app pozadine: greška pri čuvanju %s: %v", kljuc, err)
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
}
|
|
|
|
middleware.SetFlash(w, r, h.DB, "uspeh", "Izgled pozadine aplikacije je sačuvan.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
}
|
|
|
|
// SacuvajLoginPozadinaStilove čuva vrednosti zamućenja i prozirnosti pozadine login stranice
|
|
func (h *Handler) SacuvajLoginPozadinaStilove(w http.ResponseWriter, r *http.Request) {
|
|
k := middleware.KorisnikIzKonteksta(r.Context())
|
|
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.login_pozadina") {
|
|
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
|
return
|
|
}
|
|
if err := r.ParseForm(); err != nil {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čitanju forme.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
blurPozadineStr := r.FormValue("blur_pozadine")
|
|
blurKarticeStr := r.FormValue("blur_kartice")
|
|
opacityStr := r.FormValue("opacity")
|
|
|
|
blurPozadineVal, err := strconv.Atoi(blurPozadineStr)
|
|
if err != nil || blurPozadineVal < 0 || blurPozadineVal > 20 {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost zamućenja pozadine.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
blurKarticeVal, err := strconv.Atoi(blurKarticeStr)
|
|
if err != nil || blurKarticeVal < 0 || blurKarticeVal > 20 {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost zamućenja kartice.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
opacityVal, err := strconv.Atoi(opacityStr)
|
|
if err != nil || opacityVal < 0 || opacityVal > 80 {
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost prozirnosti.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
for kljuc, vrednost := range map[string]string{
|
|
"login_pozadina_blur_pozadine": blurPozadineStr,
|
|
"login_pozadina_blur_kartice": blurKarticeStr,
|
|
"login_pozadina_opacity": opacityStr,
|
|
} {
|
|
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, kljuc, vrednost); err != nil {
|
|
log.Printf("stilovi login pozadine: greška pri čuvanju %s: %v", kljuc, err)
|
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
return
|
|
}
|
|
}
|
|
|
|
middleware.SetFlash(w, r, h.DB, "uspeh", "Izgled pozadine prijave je sačuvan.")
|
|
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
|
}
|
|
|
|
// napuniPodaciPodesavanja učitava sva podešavanja i kreira strukturu za template
|
|
func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (PodaciPodesavanja, error) {
|
|
podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
|
if err != nil {
|
|
return PodaciPodesavanja{}, err
|
|
}
|
|
ps := h.popuniPodaciStranice(r, podesavanja)
|
|
ps.Stranica = "podesavanja"
|
|
ps.NaslovStranice = naslov
|
|
return PodaciPodesavanja{
|
|
PodaciStranice: ps,
|
|
NazivFirme: podesavanja["naziv_firme"],
|
|
Podnazlov: podesavanja["podnazlov"],
|
|
Adresa: podesavanja["adresa"],
|
|
Telefon: podesavanja["telefon"],
|
|
PIB: podesavanja["pib"],
|
|
LogoTip: podesavanja["logo_tip"],
|
|
LogoPutanja: podesavanja["logo_putanja"],
|
|
Tema: podesavanja["tema"],
|
|
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
|
BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno",
|
|
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"
|
|
}(),
|
|
AppPozadina: podesavanja["app_pozadina"],
|
|
AppPozadinaOpacity: func() string {
|
|
if v := podesavanja["app_pozadina_opacity"]; v != "" {
|
|
return v
|
|
}
|
|
return "50"
|
|
}(),
|
|
AppPozadinaBlur: func() string {
|
|
if v := podesavanja["app_pozadina_blur"]; v != "" {
|
|
return v
|
|
}
|
|
return "12"
|
|
}(),
|
|
AppPozadinaBlurPozadine: func() string {
|
|
if v := podesavanja["app_pozadina_blur_pozadine"]; v != "" {
|
|
return v
|
|
}
|
|
return "0"
|
|
}(),
|
|
}, nil
|
|
}
|
|
|
|
// PodesavanjaOpste renderuje stranicu sa opštim podešavanjima (firma i logo)
|
|
func (h *Handler) PodesavanjaOpste(w http.ResponseWriter, r *http.Request) {
|
|
k := middleware.KorisnikIzKonteksta(r.Context())
|
|
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.pregled") {
|
|
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
|
return
|
|
}
|
|
podaci, err := h.napuniPodaciPodesavanja(r, "Podešavanja — Opšte")
|
|
if err != nil {
|
|
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
h.renderujTemplate(w, "podesavanja_opste", podaci)
|
|
}
|
|
|
|
// PodesavanjaIzgled renderuje stranicu sa podešavanjima izgleda (pozadine i tema)
|
|
func (h *Handler) PodesavanjaIzgled(w http.ResponseWriter, r *http.Request) {
|
|
k := middleware.KorisnikIzKonteksta(r.Context())
|
|
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.pregled") {
|
|
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
|
return
|
|
}
|
|
podaci, err := h.napuniPodaciPodesavanja(r, "Podešavanja — Izgled")
|
|
if err != nil {
|
|
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
h.renderujTemplate(w, "podesavanja_izgled", podaci)
|
|
}
|
|
|
|
// PodesavanjaSistem renderuje stranicu sa sistemskim podešavanjima (backup)
|
|
func (h *Handler) PodesavanjaSistem(w http.ResponseWriter, r *http.Request) {
|
|
k := middleware.KorisnikIzKonteksta(r.Context())
|
|
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.pregled") {
|
|
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
|
return
|
|
}
|
|
podaci, err := h.napuniPodaciPodesavanja(r, "Podešavanja — Sistem")
|
|
if err != nil {
|
|
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
h.renderujTemplate(w, "podesavanja_sistem", podaci)
|
|
}
|
|
|
|
// PromeniTemu menja aktivnu temu i vraća na prethodnu stranicu
|
|
func (h *Handler) PromeniTemu(w http.ResponseWriter, r *http.Request) {
|
|
tema := chi.URLParam(r, "tema")
|
|
|
|
// proveravamo da li je validna tema
|
|
validne := map[string]bool{
|
|
"tamna": true, "svetla": true,
|
|
}
|
|
if !validne[tema] {
|
|
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "tema", tema); err != nil {
|
|
http.Error(w, "Greška pri promeni teme", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// vraćamo se na stranicu sa koje je kliknut dugmić
|
|
referer := r.Header.Get("Referer")
|
|
if referer == "" {
|
|
referer = "/dashboard"
|
|
}
|
|
http.Redirect(w, r, referer, http.StatusSeeOther)
|
|
}
|