Bezbednost — security headers, fail2ban logovanje, bruteforce zaštita, CSRF zaštita
This commit is contained in:
@@ -13,17 +13,18 @@ import (
|
||||
|
||||
// Handler drži zavisnosti koje su potrebne svim handlerima
|
||||
type Handler struct {
|
||||
DB *sql.DB
|
||||
Artikli db.ArtikalRepository
|
||||
KategorijeRepo db.KategorijaRepository
|
||||
DobavljaciRepo db.DobavljacRepository
|
||||
NabavkeRepo db.NabavkaRepository
|
||||
KlijentiRepo db.KlijentRepository
|
||||
ServisRepo db.ServisRepository
|
||||
ProdajaRepo db.ProdajaRepository
|
||||
DB *sql.DB
|
||||
Artikli db.ArtikalRepository
|
||||
KategorijeRepo db.KategorijaRepository
|
||||
DobavljaciRepo db.DobavljacRepository
|
||||
NabavkeRepo db.NabavkaRepository
|
||||
KlijentiRepo db.KlijentRepository
|
||||
ServisRepo db.ServisRepository
|
||||
ProdajaRepo db.ProdajaRepository
|
||||
KorisniciRepo db.KorisniciRepository
|
||||
SesijeRepo db.SesijeRepository
|
||||
PodsetniciFRepo db.PodsetnikRepository
|
||||
PokusajiRepo db.PokusajiPrijaveRepository
|
||||
Verzija string
|
||||
Templates map[string]*template.Template
|
||||
}
|
||||
@@ -42,6 +43,7 @@ func Novi(baza *sql.DB) *Handler {
|
||||
KorisniciRepo: sqlite.NoviKorisniciRepo(baza),
|
||||
SesijeRepo: sqlite.NoviSesijeRepo(baza),
|
||||
PodsetniciFRepo: sqlite.NoviPodsetnikRepo(baza),
|
||||
PokusajiRepo: sqlite.NoviPokusajiPrijaveRepo(baza),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,5 +62,6 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
|
||||
ps.KorisnikIme = k.KorisnickoIme
|
||||
ps.KorisnikUloga = k.Uloga
|
||||
}
|
||||
ps.CsrfToken = middleware.CsrfToken(r.Context())
|
||||
return ps
|
||||
}
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"ntech/internal/auth"
|
||||
"ntech/internal/middleware"
|
||||
)
|
||||
|
||||
const imeKolacica = "ntech_sesija"
|
||||
const trajanjeSeije = 7 * 24 * time.Hour
|
||||
const trajanjePredSeije = 5 * time.Minute
|
||||
|
||||
const maxNeuspehaPrijave = 5
|
||||
const prozorPrijave = 15 * time.Minute
|
||||
const trajanjeZakljucavanja = 30 * time.Minute
|
||||
|
||||
// PrikazPrijave renderuje formu za prijavu
|
||||
func (h *Handler) PrikazPrijave(w http.ResponseWriter, r *http.Request) {
|
||||
// ako nema korisnika, preusmeri na setup wizard
|
||||
postoji, err := h.KorisniciRepo.PostojiIjedan(r.Context())
|
||||
if err == nil && !postoji {
|
||||
http.Redirect(w, r, "/setup", http.StatusSeeOther)
|
||||
@@ -22,7 +29,8 @@ func (h *Handler) PrikazPrijave(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
greska := r.URL.Query().Get("greska")
|
||||
h.renderujStandalone(w, "prijava", map[string]any{
|
||||
"Greska": greska,
|
||||
"Greska": greska,
|
||||
"CsrfToken": middleware.CsrfToken(r.Context()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,24 +41,47 @@ func (h *Handler) Prijava(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ip := izvuciIP(r.RemoteAddr)
|
||||
korisnickoIme := r.FormValue("korisnicko_ime")
|
||||
lozinka := r.FormValue("lozinka")
|
||||
|
||||
// bruteforce provera — blokira IP posle maxNeuspehaPrijave neuspelih pokušaja
|
||||
od := time.Now().Add(-prozorPrijave)
|
||||
n, _ := h.PokusajiRepo.BrojNeuspeha(r.Context(), ip, od)
|
||||
if n >= maxNeuspehaPrijave {
|
||||
if preostalo, zaklj := h.preostaloBruteforce(r.Context(), ip, od); zaklj {
|
||||
auth.LogZaklucano(ip, korisnickoIme)
|
||||
h.renderujStandalone(w, "prijava", map[string]any{
|
||||
"Greska": "zakljucano",
|
||||
"Preostalo": preostalo,
|
||||
"CsrfToken": middleware.CsrfToken(r.Context()),
|
||||
})
|
||||
return
|
||||
}
|
||||
// zaključavanje je isteklo, nastavljamo normalnu obradu
|
||||
}
|
||||
|
||||
korisnik, err := h.KorisniciRepo.DohvatiPoImenu(r.Context(), korisnickoIme)
|
||||
if err != nil || !korisnik.Aktivan {
|
||||
_ = h.PokusajiRepo.Zabeleži(r.Context(), ip, korisnickoIme, false)
|
||||
auth.LogNeuspehPrijave(ip, korisnickoIme, "wrong_user")
|
||||
http.Redirect(w, r, "/prijava?greska=1", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if !auth.ProveriLozinku(korisnik.LozinkaHash, lozinka) {
|
||||
_ = h.PokusajiRepo.Zabeleži(r.Context(), ip, korisnickoIme, false)
|
||||
auth.LogNeuspehPrijave(ip, korisnickoIme, "wrong_password")
|
||||
http.Redirect(w, r, "/prijava?greska=1", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
_ = h.PokusajiRepo.Zabeleži(r.Context(), ip, korisnickoIme, true)
|
||||
auth.LogUspehPrijave(ip, korisnickoIme)
|
||||
|
||||
token := auth.GenerisiToken()
|
||||
|
||||
if korisnik.TotpTajna != "" {
|
||||
// kreira privremenu sesiju koja čeka TOTP verifikaciju
|
||||
if err := h.SesijeRepo.Kreiraj(r.Context(), korisnik.ID, token, time.Now().Add(trajanjePredSeije), false); err != nil {
|
||||
http.Redirect(w, r, "/prijava?greska=2", http.StatusSeeOther)
|
||||
return
|
||||
@@ -60,7 +91,6 @@ func (h *Handler) Prijava(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// direktna sesija bez TOTP
|
||||
if err := h.SesijeRepo.Kreiraj(r.Context(), korisnik.ID, token, time.Now().Add(trajanjeSeije), true); err != nil {
|
||||
http.Redirect(w, r, "/prijava?greska=2", http.StatusSeeOther)
|
||||
return
|
||||
@@ -84,7 +114,8 @@ func (h *Handler) PrikazTotp(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
greska := r.URL.Query().Get("greska")
|
||||
h.renderujStandalone(w, "totp_provera", map[string]any{
|
||||
"Greska": greska,
|
||||
"Greska": greska,
|
||||
"CsrfToken": middleware.CsrfToken(r.Context()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -133,7 +164,8 @@ func (h *Handler) PrikazSetupa(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
greska := r.URL.Query().Get("greska")
|
||||
h.renderujStandalone(w, "setup", map[string]any{
|
||||
"Greska": greska,
|
||||
"Greska": greska,
|
||||
"CsrfToken": middleware.CsrfToken(r.Context()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -188,6 +220,33 @@ func (h *Handler) Odjava(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// preostaloBruteforce vraća formatiranu poruku o preostatom vremenu zaključavanja i bool da li je IP još zaključan
|
||||
func (h *Handler) preostaloBruteforce(ctx context.Context, ip string, od time.Time) (string, bool) {
|
||||
vreme, ok, _ := h.PokusajiRepo.VremePoslednjeg(ctx, ip, od)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
d := time.Until(vreme.Add(trajanjeZakljucavanja)).Round(time.Second)
|
||||
if d <= 0 {
|
||||
return "", false
|
||||
}
|
||||
min := int(d.Minutes())
|
||||
sek := int(d.Seconds()) % 60
|
||||
if min > 0 {
|
||||
return fmt.Sprintf("%d min %d sek", min, sek), true
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return addr
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
func napraviKolacic(token string, istice time.Time) *http.Cookie {
|
||||
return &http.Cookie{
|
||||
Name: imeKolacica,
|
||||
@@ -198,4 +257,3 @@ func napraviKolacic(token string, istice time.Time) *http.Cookie {
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user