- migracija 102: indeksi na FK kolonama (stavke_prodaje, servisni_delovi,
servisni_potrazivani_delovi, magacinske_promene, stavke_nabavke, pdv_kir,
pdv_kpr) — potvrđeno kreiranje i idempotentnost
- BrojKriticnihZaliha/KriticneZalihe filtriraju samo proizvode i aktivne
artikle, usklađeno sa ArtikalRepo.Lista(SamoKriticni)
- url.QueryEscape na query string u magacin.go/klijent.go (paginacija)
- TestFiskalizacije/BeStatus vraćaju generičku poruku, detalj u slog
- SetFlash loguje grešku umesto tihog ignorisanja
- os.WriteFile/os.MkdirAll u main.go proveravaju grešku
- dummyHash generisanje panikuje na startu ako bcrypt ikad zakaže
(umesto tihog sloma anti-enumeracije)
- PrihvatiPredlozene proverava redovi.Err()
- /healthz endpoint (DB ping)
- hx-boost na "Osveži" linkovima (blokirane_ip, fiskal_pazar)
- MaxBytesReader (4KB) na javne /status/{token}/* POST rute
- komentar u auth.go zašto postoje dva flash mehanizma (namerno, ne spojeno)
Provereno: jePrivatnaAdresa ne radi DNS lookup (nema net.LookupIP u repou),
pa DNS-rebinding nalaz iz pregleda nije primenljiv na trenutni kod.
171 lines
5.9 KiB
Go
171 lines
5.9 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"ntech/internal/db/sqlite"
|
|
"ntech/internal/model"
|
|
)
|
|
|
|
type kontekstKljuc string
|
|
|
|
// KljucKorisnika je ključ za korisnika u request contextu
|
|
const KljucKorisnika kontekstKljuc = "korisnik"
|
|
|
|
// RequireAuth je chi middleware koji proverava sesiju i injektuje korisnika u context.
|
|
// totpKljuc je ključ za dešifrovanje TOTP tajne (korisRepo ga koristi pri čitanju).
|
|
func RequireAuth(db *sql.DB, totpKljuc []byte) func(http.Handler) http.Handler {
|
|
sesijeRepo := sqlite.NoviSesijeRepo(db)
|
|
korisRepo := sqlite.NoviKorisniciRepo(db, totpKljuc)
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
kolacic, err := r.Cookie("ntech_sesija")
|
|
if err != nil {
|
|
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
sesija, err := sesijeRepo.DohvatiPoTokenu(r.Context(), kolacic.Value)
|
|
if err != nil {
|
|
// nevažeći token — briši kolačić i preusmeri
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: "ntech_sesija",
|
|
Value: "",
|
|
Path: "/",
|
|
Expires: time.Unix(0, 0),
|
|
MaxAge: -1,
|
|
Secure: os.Getenv("NTECH_ENV") == "production" || os.Getenv("NTECH_ENV") == "demo",
|
|
HttpOnly: true,
|
|
})
|
|
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// proveri da li je sesija istekla
|
|
if time.Now().After(sesija.DatumIsteka) {
|
|
_ = sesijeRepo.Obrisi(r.Context(), kolacic.Value)
|
|
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// ako čeka TOTP verifikaciju, preusmeri na TOTP stranicu
|
|
if !sesija.TotpPotvrdjeno {
|
|
http.Redirect(w, r, "/prijava/totp", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
korisnik, err := korisRepo.DohvatiPoID(r.Context(), sesija.KorisnikID)
|
|
if err != nil || !korisnik.Aktivan {
|
|
_ = sesijeRepo.Obrisi(r.Context(), kolacic.Value)
|
|
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
ctx := context.WithValue(r.Context(), KljucKorisnika, korisnik)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
}
|
|
|
|
// KorisnikIzKonteksta vraća trenutno prijavljenog korisnika iz konteksta
|
|
func KorisnikIzKonteksta(ctx context.Context) *model.Korisnik {
|
|
k, _ := ctx.Value(KljucKorisnika).(*model.Korisnik)
|
|
return k
|
|
}
|
|
|
|
// JeAdmin proverava da li korisnik ima admin ili superadmin ulogu
|
|
func JeAdmin(k *model.Korisnik) bool {
|
|
if k == nil {
|
|
return false
|
|
}
|
|
return k.Uloga == "admin" || k.Uloga == "superadmin"
|
|
}
|
|
|
|
// RequireSuperAdmin je middleware koji propušta samo superadmin korisnike
|
|
func RequireSuperAdmin(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
k := KorisnikIzKonteksta(r.Context())
|
|
if k == nil || k.Uloga != "superadmin" {
|
|
postaviFlashGresku(w, "Nemate dozvolu za ovu stranicu.")
|
|
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// RequireAdmin je middleware koji propušta admin i superadmin korisnike
|
|
func RequireAdmin(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
k := KorisnikIzKonteksta(r.Context())
|
|
if k == nil || (k.Uloga != "admin" && k.Uloga != "superadmin") {
|
|
postaviFlashGresku(w, "Nemate dozvolu za ovu stranicu.")
|
|
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// RequireDozvola je middleware koji propušta korisnika samo ako njegova uloga
|
|
// ima traženu akciju. Provera je DB-backed (prosleđuje se DozvoleRepo.ImaDozvolu),
|
|
// tako da poštuje izmene matrice dozvola. Koristi se na nivou rute za "pregled"
|
|
// stranica, umesto ponavljanja iste provere u svakom handleru.
|
|
func RequireDozvola(proveri func(ctx context.Context, uloga, akcija string) bool, akcija string) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
k := KorisnikIzKonteksta(r.Context())
|
|
if k == nil || !proveri(r.Context(), k.Uloga, akcija) {
|
|
postaviFlashGresku(w, "Nemate dozvolu za ovu stranicu.")
|
|
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// RequireDozvolaMut je kao RequireDozvola, ali namenjen mutirajućim rutama
|
|
// (POST i akcije brisanja). Na odbijanje vraća 403 sa srpskom porukom umesto
|
|
// redirecta — isto kao handler.zahtevajDozvolu, pa ne kvari AJAX/fetch pozive.
|
|
// Postavljanjem ove provere na ruteru, zaštita mutacije više ne zavisi od toga
|
|
// da li je programer dodao proveru u samom handleru.
|
|
func RequireDozvolaMut(proveri func(ctx context.Context, uloga, akcija string) bool, akcija string) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
k := KorisnikIzKonteksta(r.Context())
|
|
if k == nil || !proveri(r.Context(), k.Uloga, akcija) {
|
|
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// postaviFlashGresku upisuje jednokratnu poruku o grešci u kolačić. Namerno
|
|
// odvojen mehanizam od SetFlash/GetFlash (flash.go): ove funkcije se pozivaju
|
|
// iz middleware-a koji nema pristup *sql.DB, a redirect je uvek na /dashboard
|
|
// (jedino mesto koje čita ovaj kolačić) — pa kolačić bez DB upisa dovoljan.
|
|
// SetFlash/GetFlash (kolona sesije.flash) koristi se u handlerima koji imaju
|
|
// DB i redirect na proizvoljnu stranicu.
|
|
func postaviFlashGresku(w http.ResponseWriter, poruka string) {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: "ntech_flash_greska",
|
|
Value: poruka,
|
|
Path: "/",
|
|
MaxAge: 60,
|
|
HttpOnly: true,
|
|
Secure: os.Getenv("NTECH_ENV") == "production" || os.Getenv("NTECH_ENV") == "demo",
|
|
})
|
|
}
|
|
|
|
// ErrNijePrijavljen se vraća kada korisnik nije u contextu
|
|
var ErrNijePrijavljen = errors.New("korisnik nije prijavljen")
|