Evidencija prijava — login_istorija tabela, logovanje svih pokušaja, stranica istorije po korisniku, WebAuthn shema
This commit is contained in:
@@ -111,6 +111,12 @@ type PokusajiPrijaveRepository interface {
|
||||
ObrisiStare(ctx context.Context, pre time.Time) error
|
||||
}
|
||||
|
||||
// LoginIstorijsaRepository definiše operacije nad evidencijom prijava
|
||||
type LoginIstorijsaRepository interface {
|
||||
Zabeleži(ctx context.Context, korisnikID *int64, ip, userAgent, razlog string, uspeh bool) error
|
||||
ListaZaKorisnika(ctx context.Context, korisnikID int64, limit int) ([]*model.LoginPokusaj, error)
|
||||
}
|
||||
|
||||
// PodsetnikRepository definiše operacije nad podsetnicima
|
||||
type PodsetnikRepository interface {
|
||||
Lista(ctx context.Context, filter PodsetnikFilter) ([]model.Podsetnik, error)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"ntech/internal/model"
|
||||
)
|
||||
|
||||
// LoginIstorijsaRepo implementira db.LoginIstorijsaRepository nad SQLite
|
||||
type LoginIstorijsaRepo struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NoviLoginIstorijsaRepo kreira novi repozitorijum za evidenciju prijava
|
||||
func NoviLoginIstorijsaRepo(db *sql.DB) *LoginIstorijsaRepo {
|
||||
return &LoginIstorijsaRepo{db: db}
|
||||
}
|
||||
|
||||
// Zabeleži upisuje pokušaj prijave u evidenciju
|
||||
func (r *LoginIstorijsaRepo) Zabeleži(ctx context.Context, korisnikID *int64, ip, userAgent, razlog string, uspeh bool) error {
|
||||
u := 0
|
||||
if uspeh {
|
||||
u = 1
|
||||
}
|
||||
var kid sql.NullInt64
|
||||
if korisnikID != nil {
|
||||
kid = sql.NullInt64{Valid: true, Int64: *korisnikID}
|
||||
}
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
"INSERT INTO login_istorija (korisnik_id, ip, user_agent, uspeh, razlog) VALUES (?, ?, ?, ?, ?)",
|
||||
kid, ip, userAgent, u, razlog,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: LoginIstorijsaRepo.Zabeleži: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListaZaKorisnika vraća poslednjih N zapisa za datog korisnika
|
||||
func (r *LoginIstorijsaRepo) ListaZaKorisnika(ctx context.Context, korisnikID int64, limit int) ([]*model.LoginPokusaj, error) {
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT id, korisnik_id, ip, user_agent, uspeh, razlog, vreme
|
||||
FROM login_istorija
|
||||
WHERE korisnik_id = ?
|
||||
ORDER BY vreme DESC
|
||||
LIMIT ?`,
|
||||
korisnikID, limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: LoginIstorijsaRepo.ListaZaKorisnika: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var lista []*model.LoginPokusaj
|
||||
for rows.Next() {
|
||||
p := &model.LoginPokusaj{}
|
||||
var kid sql.NullInt64
|
||||
var u int
|
||||
var s string
|
||||
if err := rows.Scan(&p.ID, &kid, &p.IP, &p.UserAgent, &u, &p.Razlog, &s); err != nil {
|
||||
return nil, fmt.Errorf("ntech: LoginIstorijsaRepo.ListaZaKorisnika: scan: %w", err)
|
||||
}
|
||||
if kid.Valid {
|
||||
p.KorisnikID = &kid.Int64
|
||||
}
|
||||
p.Uspeh = u == 1
|
||||
p.Vreme, _ = time.ParseInLocation("2006-01-02 15:04:05", s, time.UTC)
|
||||
lista = append(lista, p)
|
||||
}
|
||||
return lista, rows.Err()
|
||||
}
|
||||
@@ -20,6 +20,12 @@ type podaciAdminKorisnici struct {
|
||||
Sacuvano bool
|
||||
}
|
||||
|
||||
type podaciLoginIstorija struct {
|
||||
model.PodaciStranice
|
||||
PrikazKorisnik model.Korisnik
|
||||
Istorija []*model.LoginPokusaj
|
||||
}
|
||||
|
||||
type podaciAdminProfil struct {
|
||||
model.PodaciStranice
|
||||
Greska string
|
||||
@@ -333,6 +339,44 @@ func (h *Handler) AdminTotpDeaktivacija(w http.ResponseWriter, r *http.Request)
|
||||
http.Redirect(w, r, "/admin/profil?sacuvano=totp_off", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// AdminLoginIstorija prikazuje evidenciju prijava za datog korisnika
|
||||
func (h *Handler) AdminLoginIstorija(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !middleware.JeAdmin(k) {
|
||||
http.Error(w, "Pristup odbijen", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
korisnik, err := h.KorisniciRepo.DohvatiPoID(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Error(w, "Korisnik nije pronađen", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
istorija, err := h.LoginIstorijsaRepo.ListaZaKorisnika(r.Context(), id, 50)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju istorije", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "admin"
|
||||
ps.NaslovStranice = "Istorija prijava — " + korisnik.KorisnickoIme
|
||||
|
||||
h.renderujTemplate(w, "admin_login_istorija", podaciLoginIstorija{
|
||||
PodaciStranice: ps,
|
||||
PrikazKorisnik: *korisnik,
|
||||
Istorija: istorija,
|
||||
})
|
||||
}
|
||||
|
||||
// parseBoolForm čita boolean vrednost iz forme
|
||||
func parseBoolForm(s string) bool {
|
||||
b, _ := strconv.ParseBool(s)
|
||||
|
||||
@@ -24,8 +24,9 @@ type Handler struct {
|
||||
KorisniciRepo db.KorisniciRepository
|
||||
SesijeRepo db.SesijeRepository
|
||||
PodsetniciFRepo db.PodsetnikRepository
|
||||
PokusajiRepo db.PokusajiPrijaveRepository
|
||||
Verzija string
|
||||
PokusajiRepo db.PokusajiPrijaveRepository
|
||||
LoginIstorijsaRepo db.LoginIstorijsaRepository
|
||||
Verzija string
|
||||
Templates map[string]*template.Template
|
||||
}
|
||||
|
||||
@@ -43,7 +44,8 @@ func Novi(baza *sql.DB) *Handler {
|
||||
KorisniciRepo: sqlite.NoviKorisniciRepo(baza),
|
||||
SesijeRepo: sqlite.NoviSesijeRepo(baza),
|
||||
PodsetniciFRepo: sqlite.NoviPodsetnikRepo(baza),
|
||||
PokusajiRepo: sqlite.NoviPokusajiPrijaveRepo(baza),
|
||||
PokusajiRepo: sqlite.NoviPokusajiPrijaveRepo(baza),
|
||||
LoginIstorijsaRepo: sqlite.NoviLoginIstorijsaRepo(baza),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ func (h *Handler) Prijava(w http.ResponseWriter, r *http.Request) {
|
||||
if n >= maxNeuspehaPrijave {
|
||||
if preostalo, zaklj := h.preostaloBruteforce(r.Context(), ip, od); zaklj {
|
||||
auth.LogZaklucano(ip, korisnickoIme)
|
||||
_ = h.LoginIstorijsaRepo.Zabeleži(r.Context(), nil, ip, r.UserAgent(), "ip_zaklucano", false)
|
||||
h.renderujStandalone(w, "prijava", map[string]any{
|
||||
"Greska": "zakljucano",
|
||||
"Preostalo": preostalo,
|
||||
@@ -62,21 +63,31 @@ func (h *Handler) Prijava(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
korisnik, err := h.KorisniciRepo.DohvatiPoImenu(r.Context(), korisnickoIme)
|
||||
if err != nil || !korisnik.Aktivan {
|
||||
if err != nil {
|
||||
_ = h.PokusajiRepo.Zabeleži(r.Context(), ip, korisnickoIme, false)
|
||||
_ = h.LoginIstorijsaRepo.Zabeleži(r.Context(), nil, ip, r.UserAgent(), "korisnik_ne_postoji", false)
|
||||
auth.LogNeuspehPrijave(ip, korisnickoIme, "wrong_user")
|
||||
http.Redirect(w, r, "/prijava?greska=1", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if !korisnik.Aktivan {
|
||||
_ = h.PokusajiRepo.Zabeleži(r.Context(), ip, korisnickoIme, false)
|
||||
_ = h.LoginIstorijsaRepo.Zabeleži(r.Context(), &korisnik.ID, ip, r.UserAgent(), "nalog_neaktivan", false)
|
||||
auth.LogNeuspehPrijave(ip, korisnickoIme, "nalog_neaktivan")
|
||||
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)
|
||||
_ = h.LoginIstorijsaRepo.Zabeleži(r.Context(), &korisnik.ID, ip, r.UserAgent(), "pogrešna_lozinka", 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)
|
||||
_ = h.LoginIstorijsaRepo.Zabeleži(r.Context(), &korisnik.ID, ip, r.UserAgent(), "", true)
|
||||
auth.LogUspehPrijave(ip, korisnickoIme)
|
||||
|
||||
token := auth.GenerisiToken()
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// LoginPokusaj predstavlja jedan zapis iz evidencije prijava
|
||||
type LoginPokusaj struct {
|
||||
ID int64
|
||||
KorisnikID *int64 // nil ako korisnik nije pronađen
|
||||
IP string
|
||||
UserAgent string
|
||||
Uspeh bool
|
||||
Razlog string
|
||||
Vreme time.Time
|
||||
}
|
||||
Reference in New Issue
Block a user