Files
GoNtech/internal/db/sqlite/pokusaji_prijave.go
T
Dasko 85a2ee1b64 Bezbednost: superadmin može da odblokira IP adrese zaključane bruteforce zaštitom
Nova stranica /admin/blokirane-ip — lista IP adresa sa previše neuspelih pokušaja
prijave (broj neuspeha, poslednji pokušaj, preostalo vreme zaključavanja) i dugme
za ručno odblokiranje. Nova dozvola 'bezbednost.odblokiraj', namerno rezervisana
isključivo za ulogu superadmin (admin je nema — odblokiranje zaobilazi bruteforce
zaštitu). Repo metode PokusajiPrijaveRepo.ListaBlokiranih/Odblokiraj.
2026-07-02 21:24:25 +02:00

130 lines
4.2 KiB
Go

package sqlite
import (
"context"
"database/sql"
"fmt"
"time"
"ntech/internal/model"
)
// PokusajiPrijaveRepo implementira db.PokusajiPrijaveRepository nad SQLite
type PokusajiPrijaveRepo struct {
db *sql.DB
}
// NoviPokusajiPrijaveRepo kreira novi repozitorijum za praćenje pokušaja prijave
func NoviPokusajiPrijaveRepo(db *sql.DB) *PokusajiPrijaveRepo {
return &PokusajiPrijaveRepo{db: db}
}
// Zabeleži upisuje novi pokušaj prijave (uspešan ili neuspešan)
func (r *PokusajiPrijaveRepo) Zabeleži(ctx context.Context, ip, korisnickoIme string, uspeh bool) error {
u := 0
if uspeh {
u = 1
}
_, err := r.db.ExecContext(ctx,
"INSERT INTO pokusaji_prijave (ip, korisnicko_ime, uspeh) VALUES (?, ?, ?)",
ip, korisnickoIme, u,
)
if err != nil {
return fmt.Errorf("ntech: PokusajiPrijaveRepo.Zabeleži: %w", err)
}
return nil
}
// BrojNeuspeha vraća broj neuspelih pokušaja za datu IP adresu od zadatog trenutka
func (r *PokusajiPrijaveRepo) BrojNeuspeha(ctx context.Context, ip string, od time.Time) (int, error) {
var n int
err := r.db.QueryRowContext(ctx,
"SELECT COUNT(*) FROM pokusaji_prijave WHERE ip = ? AND uspeh = 0 AND vreme > ?",
ip, od.UTC().Format("2006-01-02 15:04:05"),
).Scan(&n)
if err != nil {
return 0, fmt.Errorf("ntech: PokusajiPrijaveRepo.BrojNeuspeha: %w", err)
}
return n, nil
}
// VremePoslednjeg vraća vreme poslednjeg neuspelog pokušaja za datu IP adresu od zadatog trenutka
func (r *PokusajiPrijaveRepo) VremePoslednjeg(ctx context.Context, ip string, od time.Time) (time.Time, bool, error) {
var s sql.NullString
err := r.db.QueryRowContext(ctx,
"SELECT MAX(vreme) FROM pokusaji_prijave WHERE ip = ? AND uspeh = 0 AND vreme > ?",
ip, od.UTC().Format("2006-01-02 15:04:05"),
).Scan(&s)
if err != nil {
return time.Time{}, false, fmt.Errorf("ntech: PokusajiPrijaveRepo.VremePoslednjeg: %w", err)
}
if !s.Valid || s.String == "" {
return time.Time{}, false, nil
}
t, err := time.ParseInLocation("2006-01-02 15:04:05", s.String, time.UTC)
if err != nil {
return time.Time{}, false, fmt.Errorf("ntech: PokusajiPrijaveRepo.VremePoslednjeg: parse: %w", err)
}
return t, true, nil
}
// ObrisiStare briše sve zapise starije od zadatog trenutka
func (r *PokusajiPrijaveRepo) ObrisiStare(ctx context.Context, pre time.Time) error {
_, err := r.db.ExecContext(ctx,
"DELETE FROM pokusaji_prijave WHERE vreme < ?",
pre.UTC().Format("2006-01-02 15:04:05"),
)
if err != nil {
return fmt.Errorf("ntech: PokusajiPrijaveRepo.ObrisiStare: %w", err)
}
return nil
}
// ListaBlokiranih vraća IP adrese sa najmanje prag neuspelih pokušaja prijave od
// zadatog trenutka, grupisano po IP adresi, poređano od najskorije zaključane.
func (r *PokusajiPrijaveRepo) ListaBlokiranih(ctx context.Context, od time.Time, prag int) ([]model.BlokiranaIP, error) {
redovi, err := r.db.QueryContext(ctx,
`SELECT ip, COUNT(*), MAX(vreme) FROM pokusaji_prijave
WHERE uspeh = 0 AND vreme > ?
GROUP BY ip HAVING COUNT(*) >= ?
ORDER BY MAX(vreme) DESC`,
od.UTC().Format("2006-01-02 15:04:05"), prag,
)
if err != nil {
return nil, fmt.Errorf("ntech: PokusajiPrijaveRepo.ListaBlokiranih: %w", err)
}
defer redovi.Close()
var lista []model.BlokiranaIP
for redovi.Next() {
var b model.BlokiranaIP
var poslednji string
if err := redovi.Scan(&b.IP, &b.BrojNeuspeha, &poslednji); err != nil {
return nil, fmt.Errorf("ntech: PokusajiPrijaveRepo.ListaBlokiranih: scan: %w", err)
}
t, err := time.ParseInLocation("2006-01-02 15:04:05", poslednji, time.UTC)
if err != nil {
return nil, fmt.Errorf("ntech: PokusajiPrijaveRepo.ListaBlokiranih: parse vremena: %w", err)
}
b.PoslednjiPokusaj = t
lista = append(lista, b)
}
if err := redovi.Err(); err != nil {
return nil, fmt.Errorf("ntech: PokusajiPrijaveRepo.ListaBlokiranih: %w", err)
}
return lista, nil
}
// Odblokiraj briše sve neuspele pokušaje prijave za datu IP adresu — brojač
// neuspeha odmah pada na nulu, pa IP prestaje da bude zaključana.
func (r *PokusajiPrijaveRepo) Odblokiraj(ctx context.Context, ip string) error {
_, err := r.db.ExecContext(ctx,
"DELETE FROM pokusaji_prijave WHERE ip = ? AND uspeh = 0",
ip,
)
if err != nil {
return fmt.Errorf("ntech: PokusajiPrijaveRepo.Odblokiraj: %w", err)
}
return nil
}