Evidencija prijava — login_istorija tabela, logovanje svih pokušaja, stranica istorije po korisniku, WebAuthn shema
This commit is contained in:
@@ -165,6 +165,7 @@ func main() {
|
|||||||
|
|
||||||
// admin rute
|
// admin rute
|
||||||
r.Get("/admin/korisnici", h.AdminKorisnici)
|
r.Get("/admin/korisnici", h.AdminKorisnici)
|
||||||
|
r.Get("/admin/korisnici/{id}/istorija", h.AdminLoginIstorija)
|
||||||
r.Post("/admin/korisnici/novi", h.AdminSacuvajKorisnika)
|
r.Post("/admin/korisnici/novi", h.AdminSacuvajKorisnika)
|
||||||
r.Post("/admin/korisnici/{id}/aktivan", h.AdminToggleAktivan)
|
r.Post("/admin/korisnici/{id}/aktivan", h.AdminToggleAktivan)
|
||||||
r.Post("/admin/korisnici/{id}/uloga", h.AdminPromeniUlogu)
|
r.Post("/admin/korisnici/{id}/uloga", h.AdminPromeniUlogu)
|
||||||
|
|||||||
@@ -111,6 +111,12 @@ type PokusajiPrijaveRepository interface {
|
|||||||
ObrisiStare(ctx context.Context, pre time.Time) error
|
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
|
// PodsetnikRepository definiše operacije nad podsetnicima
|
||||||
type PodsetnikRepository interface {
|
type PodsetnikRepository interface {
|
||||||
Lista(ctx context.Context, filter PodsetnikFilter) ([]model.Podsetnik, error)
|
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
|
Sacuvano bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type podaciLoginIstorija struct {
|
||||||
|
model.PodaciStranice
|
||||||
|
PrikazKorisnik model.Korisnik
|
||||||
|
Istorija []*model.LoginPokusaj
|
||||||
|
}
|
||||||
|
|
||||||
type podaciAdminProfil struct {
|
type podaciAdminProfil struct {
|
||||||
model.PodaciStranice
|
model.PodaciStranice
|
||||||
Greska string
|
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)
|
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
|
// parseBoolForm čita boolean vrednost iz forme
|
||||||
func parseBoolForm(s string) bool {
|
func parseBoolForm(s string) bool {
|
||||||
b, _ := strconv.ParseBool(s)
|
b, _ := strconv.ParseBool(s)
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ type Handler struct {
|
|||||||
KorisniciRepo db.KorisniciRepository
|
KorisniciRepo db.KorisniciRepository
|
||||||
SesijeRepo db.SesijeRepository
|
SesijeRepo db.SesijeRepository
|
||||||
PodsetniciFRepo db.PodsetnikRepository
|
PodsetniciFRepo db.PodsetnikRepository
|
||||||
PokusajiRepo db.PokusajiPrijaveRepository
|
PokusajiRepo db.PokusajiPrijaveRepository
|
||||||
Verzija string
|
LoginIstorijsaRepo db.LoginIstorijsaRepository
|
||||||
|
Verzija string
|
||||||
Templates map[string]*template.Template
|
Templates map[string]*template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +44,8 @@ func Novi(baza *sql.DB) *Handler {
|
|||||||
KorisniciRepo: sqlite.NoviKorisniciRepo(baza),
|
KorisniciRepo: sqlite.NoviKorisniciRepo(baza),
|
||||||
SesijeRepo: sqlite.NoviSesijeRepo(baza),
|
SesijeRepo: sqlite.NoviSesijeRepo(baza),
|
||||||
PodsetniciFRepo: sqlite.NoviPodsetnikRepo(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 n >= maxNeuspehaPrijave {
|
||||||
if preostalo, zaklj := h.preostaloBruteforce(r.Context(), ip, od); zaklj {
|
if preostalo, zaklj := h.preostaloBruteforce(r.Context(), ip, od); zaklj {
|
||||||
auth.LogZaklucano(ip, korisnickoIme)
|
auth.LogZaklucano(ip, korisnickoIme)
|
||||||
|
_ = h.LoginIstorijsaRepo.Zabeleži(r.Context(), nil, ip, r.UserAgent(), "ip_zaklucano", false)
|
||||||
h.renderujStandalone(w, "prijava", map[string]any{
|
h.renderujStandalone(w, "prijava", map[string]any{
|
||||||
"Greska": "zakljucano",
|
"Greska": "zakljucano",
|
||||||
"Preostalo": preostalo,
|
"Preostalo": preostalo,
|
||||||
@@ -62,21 +63,31 @@ func (h *Handler) Prijava(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
korisnik, err := h.KorisniciRepo.DohvatiPoImenu(r.Context(), korisnickoIme)
|
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.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")
|
auth.LogNeuspehPrijave(ip, korisnickoIme, "wrong_user")
|
||||||
http.Redirect(w, r, "/prijava?greska=1", http.StatusSeeOther)
|
http.Redirect(w, r, "/prijava?greska=1", http.StatusSeeOther)
|
||||||
return
|
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) {
|
if !auth.ProveriLozinku(korisnik.LozinkaHash, lozinka) {
|
||||||
_ = h.PokusajiRepo.Zabeleži(r.Context(), ip, korisnickoIme, false)
|
_ = 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")
|
auth.LogNeuspehPrijave(ip, korisnickoIme, "wrong_password")
|
||||||
http.Redirect(w, r, "/prijava?greska=1", http.StatusSeeOther)
|
http.Redirect(w, r, "/prijava?greska=1", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = h.PokusajiRepo.Zabeleži(r.Context(), ip, korisnickoIme, true)
|
_ = 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)
|
auth.LogUspehPrijave(ip, korisnickoIme)
|
||||||
|
|
||||||
token := auth.GenerisiToken()
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS login_istorija (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
korisnik_id INTEGER,
|
||||||
|
ip TEXT NOT NULL DEFAULT '',
|
||||||
|
user_agent TEXT NOT NULL DEFAULT '',
|
||||||
|
uspeh INTEGER NOT NULL DEFAULT 0,
|
||||||
|
razlog TEXT NOT NULL DEFAULT '',
|
||||||
|
vreme DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (korisnik_id) REFERENCES korisnici(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_login_istorija_korisnik ON login_istorija(korisnik_id, vreme);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS webauthn_kredencijali (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
korisnik_id INTEGER NOT NULL,
|
||||||
|
credential_id TEXT NOT NULL UNIQUE,
|
||||||
|
public_key BLOB NOT NULL,
|
||||||
|
sign_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
datum_kreiranja DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (korisnik_id) REFERENCES korisnici(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
@@ -105,7 +105,11 @@
|
|||||||
<td style="padding:10px 20px;font-size:13px;color:var(--tekst-sporedni);">
|
<td style="padding:10px 20px;font-size:13px;color:var(--tekst-sporedni);">
|
||||||
{{.DatumKreiranja.Format "02.01.2006."}}
|
{{.DatumKreiranja.Format "02.01.2006."}}
|
||||||
</td>
|
</td>
|
||||||
<td style="padding:10px 20px;text-align:right;">
|
<td style="padding:10px 20px;text-align:right;white-space:nowrap;">
|
||||||
|
<a href="/admin/korisnici/{{.ID}}/istorija"
|
||||||
|
style="display:inline-block;padding:5px 12px;border:0.5px solid var(--ivica);border-radius:6px;color:var(--tekst-sporedni);font-size:12px;text-decoration:none;margin-right:6px;">
|
||||||
|
Istorija prijava
|
||||||
|
</a>
|
||||||
{{if ne .KorisnickoIme $.KorisnikIme}}
|
{{if ne .KorisnickoIme $.KorisnikIme}}
|
||||||
<form method="POST" action="/admin/korisnici/{{.ID}}/aktivan" style="display:inline;">
|
<form method="POST" action="/admin/korisnici/{{.ID}}/aktivan" style="display:inline;">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "naslov"}}Istorija prijava — NTech{{end}}
|
||||||
|
|
||||||
|
{{define "sadrzaj"}}
|
||||||
|
<div style="display:flex;flex-direction:column;gap:16px;">
|
||||||
|
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
|
||||||
|
<a href="/admin/korisnici"
|
||||||
|
style="display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border:0.5px solid var(--ivica);border-radius:8px;color:var(--tekst-sporedni);font-size:13px;text-decoration:none;">
|
||||||
|
← Nazad
|
||||||
|
</a>
|
||||||
|
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">
|
||||||
|
Istorija prijava — {{.PrikazKorisnik.KorisnickoIme}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="kartica" style="padding:0;overflow:hidden;">
|
||||||
|
<div style="padding:16px 20px;border-bottom:0.5px solid var(--ivica);display:flex;align-items:center;justify-content:space-between;">
|
||||||
|
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Poslednjih 50 pokušaja</span>
|
||||||
|
<span style="font-size:12px;color:var(--tekst-sporedni);">Vreme prikazano u UTC</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if .Istorija}}
|
||||||
|
<div style="overflow-x:auto;">
|
||||||
|
<table style="width:100%;border-collapse:collapse;">
|
||||||
|
<thead>
|
||||||
|
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||||
|
<th style="padding:10px 20px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);white-space:nowrap;">Datum i vreme</th>
|
||||||
|
<th style="padding:10px 20px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">IP adresa</th>
|
||||||
|
<th style="padding:10px 20px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Pregledač</th>
|
||||||
|
<th style="padding:10px 20px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Status</th>
|
||||||
|
<th style="padding:10px 20px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Razlog</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Istorija}}
|
||||||
|
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||||
|
<td style="padding:10px 20px;font-size:13px;color:var(--tekst-sporedni);white-space:nowrap;">
|
||||||
|
{{.Vreme.Format "02.01.2006. 15:04:05"}}
|
||||||
|
</td>
|
||||||
|
<td style="padding:10px 20px;font-size:13px;color:var(--tekst-glavni);font-family:monospace;">
|
||||||
|
{{.IP}}
|
||||||
|
</td>
|
||||||
|
<td style="padding:10px 20px;font-size:12px;color:var(--tekst-sporedni);max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
|
||||||
|
{{.UserAgent}}
|
||||||
|
</td>
|
||||||
|
<td style="padding:10px 20px;text-align:center;">
|
||||||
|
{{if .Uspeh}}
|
||||||
|
<span style="display:inline-block;padding:2px 10px;border-radius:20px;background:#f0fdf4;color:#16a34a;font-size:11px;font-weight:500;">Uspeh</span>
|
||||||
|
{{else}}
|
||||||
|
<span style="display:inline-block;padding:2px 10px;border-radius:20px;background:#fef2f2;color:#dc2626;font-size:11px;font-weight:500;">Neuspeh</span>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
<td style="padding:10px 20px;font-size:13px;color:var(--tekst-sporedni);">
|
||||||
|
{{if eq .Razlog "korisnik_ne_postoji"}}Korisnik ne postoji
|
||||||
|
{{else if eq .Razlog "nalog_neaktivan"}}Nalog neaktivan
|
||||||
|
{{else if eq .Razlog "pogrešna_lozinka"}}Pogrešna lozinka
|
||||||
|
{{else if eq .Razlog "ip_zaklucano"}}IP zaključano
|
||||||
|
{{else if .Uspeh}}—
|
||||||
|
{{else}}{{.Razlog}}
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div style="padding:40px 20px;text-align:center;color:var(--tekst-sporedni);font-size:14px;">
|
||||||
|
Nema zabeleženih pokušaja prijave za ovog korisnika.
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
Reference in New Issue
Block a user