Dodato go:embed - disk-first logika za statičke fajlove i šablone
This commit is contained in:
@@ -82,18 +82,13 @@ func (h *Handler) AdminSacuvajKorisnika(w http.ResponseWriter, r *http.Request)
|
||||
lozinka := r.FormValue("lozinka")
|
||||
uloga := r.FormValue("uloga")
|
||||
|
||||
validneUloge := map[string]bool{"superadmin": true, "admin": true, "radnik": true}
|
||||
// superadmin uloga se ne može kreirati kroz interfejs — jedini superadmin postoji od setup-a
|
||||
validneUloge := map[string]bool{"admin": true, "radnik": true}
|
||||
if len(ime) < 3 || len(lozinka) < 8 || !validneUloge[uloga] {
|
||||
http.Redirect(w, r, "/admin/korisnici?greska=1", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// superadmin može kreirati samo admin i radnik (ne drugog superadmina) osim ako je jedini superadmin
|
||||
if uloga == "superadmin" && k.Uloga != "superadmin" {
|
||||
http.Error(w, "Pristup odbijen", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := auth.HashujLozinku(lozinka)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/admin/korisnici?greska=2", http.StatusSeeOther)
|
||||
@@ -161,13 +156,33 @@ func (h *Handler) AdminPromeniUlogu(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// superadmin ne može menjati svoju vlastitu ulogu
|
||||
if id == k.ID {
|
||||
http.Redirect(w, r, "/admin/korisnici?greska=3", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
uloga := r.FormValue("uloga")
|
||||
validneUloge := map[string]bool{"superadmin": true, "admin": true, "radnik": true}
|
||||
// dozvoljena je samo promena između admin i radnik; superadmin uloga se ne može dodeliti ni ukloniti
|
||||
validneUloge := map[string]bool{"admin": true, "radnik": true}
|
||||
if !validneUloge[uloga] {
|
||||
http.Redirect(w, r, "/admin/korisnici?greska=1", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// dohvati korisnika da proverimo njegovu trenutnu ulogu
|
||||
ciljniKorisnik, err := h.KorisniciRepo.DohvatiPoID(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/admin/korisnici?greska=2", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// superadmin uloga se ne može menjati
|
||||
if ciljniKorisnik.Uloga == "superadmin" {
|
||||
http.Redirect(w, r, "/admin/korisnici?greska=3", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.KorisniciRepo.AzurirajUlogu(r.Context(), id, uloga); err != nil {
|
||||
http.Redirect(w, r, "/admin/korisnici?greska=2", http.StatusSeeOther)
|
||||
return
|
||||
@@ -176,6 +191,45 @@ func (h *Handler) AdminPromeniUlogu(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin/korisnici?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// AdminObrisiKorisnika briše korisnika sa ulogom radnik
|
||||
func (h *Handler) AdminObrisiKorisnika(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if k == nil || k.Uloga != "superadmin" {
|
||||
http.Error(w, "Pristup odbijen", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/admin/korisnici?greska=1", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if id == k.ID {
|
||||
http.Redirect(w, r, "/admin/korisnici?greska=3", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
ciljni, err := h.KorisniciRepo.DohvatiPoID(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/admin/korisnici?greska=2", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// dozvoljeno je brisanje samo radnika
|
||||
if ciljni.Uloga != "radnik" {
|
||||
http.Redirect(w, r, "/admin/korisnici?greska=3", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.KorisniciRepo.Obrisi(r.Context(), id); err != nil {
|
||||
http.Redirect(w, r, "/admin/korisnici?greska=2", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/admin/korisnici?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// AdminProfil prikazuje stranicu profila
|
||||
func (h *Handler) AdminProfil(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
|
||||
@@ -14,7 +14,8 @@ import (
|
||||
|
||||
// Handler drži zavisnosti koje su potrebne svim handlerima
|
||||
type Handler struct {
|
||||
DB *sql.DB
|
||||
DB *sql.DB
|
||||
PutanjaBaze string
|
||||
Artikli db.ArtikalRepository
|
||||
KategorijeRepo db.KategorijaRepository
|
||||
DobavljaciRepo db.DobavljacRepository
|
||||
@@ -51,6 +52,23 @@ func Novi(baza *sql.DB) *Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// reinicijalzijRepozitorijume zamenjuje sve repozitorijume posle obnove baze
|
||||
func (h *Handler) reinicijalzijRepozitorijume(novaDB *sql.DB) {
|
||||
h.DB = novaDB
|
||||
h.Artikli = sqlite.NoviArtikalRepo(novaDB)
|
||||
h.KategorijeRepo = sqlite.NovaKategorijaRepo(novaDB)
|
||||
h.DobavljaciRepo = sqlite.NoviDobavljacRepo(novaDB)
|
||||
h.NabavkeRepo = sqlite.NoviNabavkaRepo(novaDB)
|
||||
h.KlijentiRepo = sqlite.NoviKlijentRepo(novaDB)
|
||||
h.ServisRepo = sqlite.NoviServisRepo(novaDB)
|
||||
h.ProdajaRepo = sqlite.NoviProdajaRepo(novaDB)
|
||||
h.KorisniciRepo = sqlite.NoviKorisniciRepo(novaDB)
|
||||
h.SesijeRepo = sqlite.NoviSesijeRepo(novaDB)
|
||||
h.PodsetniciFRepo = sqlite.NoviPodsetnikRepo(novaDB)
|
||||
h.PokusajiRepo = sqlite.NoviPokusajiPrijaveRepo(novaDB)
|
||||
h.LoginIstorijsaRepo = sqlite.NoviLoginIstorijsaRepo(novaDB)
|
||||
}
|
||||
|
||||
// popuniPodaciStranice popunjava zajednička polja stranice uključujući prijavljenog korisnika
|
||||
func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]string) model.PodaciStranice {
|
||||
ps := model.PodaciStranice{
|
||||
|
||||
@@ -16,7 +16,7 @@ var bazniSabloni = []string{
|
||||
|
||||
// saSidebar su šabloni koji koriste base layout (sidebar + topbar)
|
||||
var saSidebar = []string{
|
||||
"admin_korisnici", "admin_profil",
|
||||
"admin_korisnici", "admin_profil", "admin_login_istorija",
|
||||
"dashboard",
|
||||
"dobavljaci", "dobavljac_forma",
|
||||
"izvestaji",
|
||||
@@ -79,7 +79,7 @@ func (h *Handler) renderujTemplate(w http.ResponseWriter, ime string, podaci any
|
||||
copy(fajlovi, bazniSabloni)
|
||||
fajlovi = append(fajlovi, "web/templates/stranice/"+ime+".html")
|
||||
var err error
|
||||
if tmpl, err = template.ParseFiles(fajlovi...); err != nil {
|
||||
if tmpl, err = template.ParseFS(h.TemplatesFS, fajlovi...); err != nil {
|
||||
log.Printf("greška pri parsiranju šablona %s: %v", ime, err)
|
||||
http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -106,7 +106,7 @@ func (h *Handler) renderujStandalone(w http.ResponseWriter, ime string, podaci a
|
||||
tmpl = t
|
||||
} else {
|
||||
var err error
|
||||
if tmpl, err = template.ParseFiles("web/templates/stranice/" + ime + ".html"); err != nil {
|
||||
if tmpl, err = template.ParseFS(h.TemplatesFS, "web/templates/stranice/"+ime+".html"); err != nil {
|
||||
log.Printf("greška pri parsiranju šablona %s: %v", ime, err)
|
||||
http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -7,10 +7,12 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ntech/internal/db/sqlite"
|
||||
ntechsqlite "ntech/internal/db/sqlite"
|
||||
"ntech/internal/model"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -29,12 +31,24 @@ type PodaciPodesavanja struct {
|
||||
Tema string
|
||||
Sacuvano bool
|
||||
Verzija string
|
||||
LogoGreska string
|
||||
LogoGreska string
|
||||
BackupVracen bool
|
||||
Backupi []BackupInfo
|
||||
}
|
||||
|
||||
// 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) {
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -54,13 +68,123 @@ func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) {
|
||||
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(),
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
@@ -82,7 +206,7 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
|
||||
if vrednost == "" {
|
||||
continue
|
||||
}
|
||||
if err := sqlite.SacuvajPodesavanje(r.Context(), h.DB, kljuc, vrednost); err != nil {
|
||||
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, kljuc, vrednost); err != nil {
|
||||
http.Error(w, "Greška pri čuvanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -109,6 +233,8 @@ func (h *Handler) BackupBaze(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// OtpremiLogo prima multipart upload slike loga i čuva je u web/static/uploads/
|
||||
func (h *Handler) OtpremiLogo(w http.ResponseWriter, r *http.Request) {
|
||||
// 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
|
||||
@@ -121,6 +247,12 @@ func (h *Handler) OtpremiLogo(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
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{
|
||||
@@ -172,8 +304,9 @@ func (h *Handler) OtpremiLogo(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
putanja := "/static/uploads/logo" + ext
|
||||
if err := sqlite.SacuvajPodesavanje(r.Context(), h.DB, "logo_putanja", putanja); err != nil {
|
||||
// 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
|
||||
@@ -195,7 +328,7 @@ func (h *Handler) PromeniTemu(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := sqlite.SacuvajPodesavanje(r.Context(), h.DB, "tema", tema); err != nil {
|
||||
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "tema", tema); err != nil {
|
||||
http.Error(w, "Greška pri promeni teme", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ntech/internal/auth"
|
||||
@@ -41,7 +42,7 @@ func (h *Handler) Prijava(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ip := izvuciIP(r.RemoteAddr)
|
||||
ip := izvuciIP(r)
|
||||
korisnickoIme := r.FormValue("korisnicko_ime")
|
||||
lozinka := r.FormValue("lozinka")
|
||||
|
||||
@@ -249,11 +250,20 @@ func (h *Handler) preostaloBruteforce(ctx context.Context, ip string, od time.Ti
|
||||
return fmt.Sprintf("%d sek", sek), true
|
||||
}
|
||||
|
||||
// izvuciIP izdvaja IP adresu iz RemoteAddr formata "ip:port"
|
||||
func izvuciIP(addr string) string {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
// izvuciIP čita pravi IP klijenta — najpre X-Real-IP (koji nginx postavlja),
|
||||
// zatim poslednji X-Forwarded-For (dodat od strane proxy-a), pa RemoteAddr
|
||||
func izvuciIP(r *http.Request) string {
|
||||
if ip := r.Header.Get("X-Real-IP"); ip != "" {
|
||||
return ip
|
||||
}
|
||||
if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
|
||||
// uzimamo poslednju vrednost — nju dodaje naš proxy, ne klijent
|
||||
parts := strings.Split(fwd, ",")
|
||||
return strings.TrimSpace(parts[len(parts)-1])
|
||||
}
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return addr
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user