feat(2fa): rezervni (jednokratni) kodovi za 2FA
Alternativa TOTP-u kada uređaj nije dostupan. Po CLAUDE.md specifikaciji: 10 kodova pri aktivaciji, čuvani kao bcrypt heš. Backend: - migracija 039 (tabela rezervni_kodovi, FK CASCADE) - auth.GenerisiRezervneKodove (Crockford base32, XXXX-XXXX) + NormalizujRezervniKod - RezervniKodoviRepository (Zameni/Iskoristi/BrojPreostalih/Obrisi) + SQLite impl - žičenje u Handler (+ reinicijalizuj) Prijava: - VerifikujTotp prvo proba TOTP, pa rezervni kod (isto polje); kod je jednokratni - totp_provera.html: input opušten (slova/crtica), napomena o rezervnom kodu Profil: - aktivacija generiše i prikazuje kodove JEDNOM; dugme Regeneriši; brojač preostalo X/10 - deaktivacija briše kodove Testovi: auth (generisanje/format/normalizacija), repo (jednokratnost/regeneracija), prijava rezervnim kodom end-to-end. Ukupno 36 test funkcija.
This commit is contained in:
@@ -220,6 +220,49 @@ func TestTotpTok(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrijavaRezervnimKodom(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
ctx := context.Background()
|
||||
id := seedKorisnik(t, h, "pera", "tajna123", "radnik")
|
||||
_ = h.KorisniciRepo.SacuvajTotpTajnu(ctx, id, "JBSWY3DPEHPK3PXP")
|
||||
|
||||
// generiši i sačuvaj rezervne kodove
|
||||
kodovi, err := auth.GenerisiRezervneKodove(5)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerisiRezervneKodove: %v", err)
|
||||
}
|
||||
var hashevi []string
|
||||
for _, kod := range kodovi {
|
||||
hash, _ := auth.HashujLozinku(kod)
|
||||
hashevi = append(hashevi, hash)
|
||||
}
|
||||
if err := h.RezervniKodoviRepo.Zameni(ctx, id, hashevi); err != nil {
|
||||
t.Fatalf("Zameni: %v", err)
|
||||
}
|
||||
|
||||
// prijava lozinkom → pred-sesija
|
||||
w := httptest.NewRecorder()
|
||||
h.Prijava(w, postPrijava("pera", "tajna123"))
|
||||
token := sesijskiKolacic(w.Result())
|
||||
|
||||
// umesto TOTP-a unesi REZERVNI kod
|
||||
form := url.Values{}
|
||||
form.Set("kod", kodovi[0])
|
||||
r := httptest.NewRequest("POST", "/prijava/totp", strings.NewReader(form.Encode()))
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
r.AddCookie(&http.Cookie{Name: imeKolacica, Value: token})
|
||||
w2 := httptest.NewRecorder()
|
||||
h.VerifikujTotp(w2, r)
|
||||
|
||||
if loc := w2.Result().Header.Get("Location"); loc != "/dashboard" {
|
||||
t.Fatalf("rezervnim kodom Location = %q, očekivano /dashboard", loc)
|
||||
}
|
||||
// kod je potrošen (jednokratni) — preostalo 4
|
||||
if n, _ := h.RezervniKodoviRepo.BrojPreostalih(ctx, id); n != 4 {
|
||||
t.Fatalf("posle upotrebe preostalo %d kodova, očekivano 4", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTotpPogresanKod(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
ctx := context.Background()
|
||||
|
||||
Reference in New Issue
Block a user