add loger, access log and bans/whitelist
This commit is contained in:
@@ -30,6 +30,87 @@ func (h *Handlers) LoginPage(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// --- Failed login tracking and automatic IP bans ---
|
||||
|
||||
const (
|
||||
defaultPwdFailuresThreshold = 5
|
||||
defaultMFAFailuresThreshold = 10
|
||||
defaultFailuresWindow = 30 * time.Minute
|
||||
defaultBanDuration = 12 * time.Hour
|
||||
)
|
||||
|
||||
// recordFailedAttempt logs a failed attempt and applies IP ban if thresholds exceeded.
|
||||
func (h *Handlers) recordFailedAttempt(c *gin.Context, typ, username string, userID *int64) {
|
||||
ip := c.ClientIP()
|
||||
var uid interface{}
|
||||
if userID != nil {
|
||||
uid = *userID
|
||||
}
|
||||
// Insert failed attempt (best-effort)
|
||||
_, _ = h.authSvc.DB.Exec(`INSERT INTO failed_logins (ip, user_id, username, type) VALUES (?,?,?,?)`, ip, uid, username, typ)
|
||||
|
||||
// Determine threshold using config overrides (fallback to defaults when zero)
|
||||
threshold := h.config.PwdFailuresThreshold
|
||||
if threshold <= 0 {
|
||||
threshold = defaultPwdFailuresThreshold
|
||||
}
|
||||
if typ == "mfa" {
|
||||
t2 := h.config.MFAFailuresThreshold
|
||||
if t2 <= 0 {
|
||||
t2 = defaultMFAFailuresThreshold
|
||||
}
|
||||
threshold = t2
|
||||
}
|
||||
// Count recent failures for this IP and type
|
||||
var cnt int
|
||||
// Window from config (minutes)
|
||||
windowMinutes := h.config.FailuresWindowMinutes
|
||||
if windowMinutes <= 0 {
|
||||
windowMinutes = int(defaultFailuresWindow / time.Minute)
|
||||
}
|
||||
cutoff := time.Now().Add(-time.Duration(windowMinutes) * time.Minute)
|
||||
_ = h.authSvc.DB.QueryRow(`SELECT COUNT(1) FROM failed_logins WHERE ip = ? AND type = ? AND created_at >= ?`, ip, typ, cutoff).Scan(&cnt)
|
||||
if cnt >= threshold {
|
||||
// Duration/permanence from config
|
||||
banHours := h.config.AutoBanDurationHours
|
||||
if banHours <= 0 {
|
||||
banHours = int(defaultBanDuration / time.Hour)
|
||||
}
|
||||
makePermanent := h.config.AutoBanPermanent
|
||||
var until interface{}
|
||||
if makePermanent {
|
||||
until = nil
|
||||
} else {
|
||||
until = time.Now().Add(time.Duration(banHours) * time.Hour)
|
||||
}
|
||||
reason := fmt.Sprintf("auto-ban: %s failures=%d within %d minutes", typ, cnt, windowMinutes)
|
||||
// Upsert ban unless whitelisted or permanent existing
|
||||
if makePermanent {
|
||||
_, _ = h.authSvc.DB.Exec(`
|
||||
INSERT INTO ip_bans (ip, reason, until, permanent, whitelisted, created_at, updated_at)
|
||||
VALUES (?, ?, NULL, 1, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(ip) DO UPDATE SET
|
||||
reason=excluded.reason,
|
||||
until=NULL,
|
||||
permanent=1,
|
||||
updated_at=CURRENT_TIMESTAMP
|
||||
WHERE ip_bans.whitelisted = 0
|
||||
`, ip, reason)
|
||||
} else {
|
||||
_, _ = h.authSvc.DB.Exec(`
|
||||
INSERT INTO ip_bans (ip, reason, until, permanent, whitelisted, created_at, updated_at)
|
||||
VALUES (?, ?, ?, 0, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(ip) DO UPDATE SET
|
||||
reason=excluded.reason,
|
||||
until=excluded.until,
|
||||
permanent=0,
|
||||
updated_at=CURRENT_TIMESTAMP
|
||||
WHERE ip_bans.whitelisted = 0 AND ip_bans.permanent = 0
|
||||
`, ip, reason, until)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isAllDigits returns true if s consists only of ASCII digits 0-9
|
||||
func isAllDigits(s string) bool {
|
||||
if s == "" {
|
||||
@@ -65,6 +146,8 @@ func (h *Handlers) MFALoginVerify(c *gin.Context) {
|
||||
code := strings.TrimSpace(c.PostForm("code"))
|
||||
if len(code) != 6 || !isAllDigits(code) {
|
||||
token, _ := c.Get("csrf_token")
|
||||
// record failed MFA attempt
|
||||
h.recordFailedAttempt(c, "mfa", "", nil)
|
||||
c.HTML(http.StatusUnauthorized, "mfa", gin.H{
|
||||
"app_name": h.config.AppName,
|
||||
"csrf_token": token,
|
||||
@@ -84,11 +167,13 @@ func (h *Handlers) MFALoginVerify(c *gin.Context) {
|
||||
uid, _ := uidAny.(int64)
|
||||
var secret string
|
||||
if err := h.authSvc.DB.QueryRow(`SELECT mfa_secret FROM users WHERE id = ?`, uid).Scan(&secret); err != nil || secret == "" {
|
||||
h.recordFailedAttempt(c, "mfa", "", &uid)
|
||||
c.HTML(http.StatusUnauthorized, "mfa", gin.H{"error": "MFA not enabled", "Page": "mfa", "ContentTemplate": "mfa_content", "ScriptsTemplate": "mfa_scripts", "app_name": h.config.AppName})
|
||||
return
|
||||
}
|
||||
if !verifyTOTP(secret, code, time.Now()) {
|
||||
token, _ := c.Get("csrf_token")
|
||||
h.recordFailedAttempt(c, "mfa", "", &uid)
|
||||
c.HTML(http.StatusUnauthorized, "mfa", gin.H{
|
||||
"app_name": h.config.AppName,
|
||||
"csrf_token": token,
|
||||
@@ -227,6 +312,8 @@ func (h *Handlers) LoginPost(c *gin.Context) {
|
||||
user, err := h.authSvc.Authenticate(username, password)
|
||||
if err != nil {
|
||||
token, _ := c.Get("csrf_token")
|
||||
// record failed password attempt
|
||||
h.recordFailedAttempt(c, "password", username, nil)
|
||||
c.HTML(http.StatusUnauthorized, "login", gin.H{
|
||||
"app_name": h.config.AppName,
|
||||
"csrf_token": token,
|
||||
|
||||
Reference in New Issue
Block a user