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,
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"crypto/rand"
|
||||
|
||||
@@ -32,6 +33,312 @@ type Handlers struct {
|
||||
authSvc *auth.Service
|
||||
}
|
||||
|
||||
// AdminBanIP bans an IP (temporary via hours or permanent)
|
||||
func (h *Handlers) AdminBanIP(c *gin.Context) {
|
||||
ip := strings.TrimSpace(c.PostForm("ip"))
|
||||
if ip == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ip is required"})
|
||||
return
|
||||
}
|
||||
reason := strings.TrimSpace(c.PostForm("reason"))
|
||||
permStr := strings.TrimSpace(c.PostForm("permanent"))
|
||||
hoursStr := strings.TrimSpace(c.PostForm("hours"))
|
||||
permanent := 0
|
||||
if permStr == "1" || strings.EqualFold(permStr, "true") {
|
||||
permanent = 1
|
||||
}
|
||||
|
||||
var until sql.NullTime
|
||||
if permanent == 0 && hoursStr != "" {
|
||||
if hInt, err := strconv.Atoi(hoursStr); err == nil && hInt > 0 {
|
||||
t := time.Now().Add(time.Duration(hInt) * time.Hour)
|
||||
until = sql.NullTime{Time: t, Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert ban
|
||||
if permanent == 1 {
|
||||
_, err := h.authSvc.DB.Exec(`INSERT INTO ip_bans (ip, reason, until, permanent, whitelisted, updated_at)
|
||||
VALUES (?, ?, NULL, 1, 0, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(ip) DO UPDATE SET reason=excluded.reason, until=NULL, permanent=1, whitelisted=0, updated_at=CURRENT_TIMESTAMP`, ip, nullIfEmpty(reason))
|
||||
if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}); return }
|
||||
} else {
|
||||
_, err := h.authSvc.DB.Exec(`INSERT INTO ip_bans (ip, reason, until, permanent, whitelisted, updated_at)
|
||||
VALUES (?, ?, ?, 0, 0, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(ip) DO UPDATE SET reason=excluded.reason, until=excluded.until, permanent=0, whitelisted=0, updated_at=CURRENT_TIMESTAMP`, ip, nullIfEmpty(reason), until)
|
||||
if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}); return }
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// AdminUnbanIP removes a ban entry (and whitelist) for an IP
|
||||
func (h *Handlers) AdminUnbanIP(c *gin.Context) {
|
||||
ip := strings.TrimSpace(c.PostForm("ip"))
|
||||
if ip == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ip is required"})
|
||||
return
|
||||
}
|
||||
if _, err := h.authSvc.DB.Exec(`DELETE FROM ip_bans WHERE ip = ?`, ip); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// AdminWhitelistIP sets or clears whitelist for an IP
|
||||
func (h *Handlers) AdminWhitelistIP(c *gin.Context) {
|
||||
ip := strings.TrimSpace(c.PostForm("ip"))
|
||||
if ip == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ip is required"})
|
||||
return
|
||||
}
|
||||
val := strings.TrimSpace(c.PostForm("value"))
|
||||
w := 0
|
||||
if val == "1" || strings.EqualFold(val, "true") { w = 1 }
|
||||
// Ensure row exists and update flags
|
||||
_, err := h.authSvc.DB.Exec(`INSERT INTO ip_bans (ip, reason, until, permanent, whitelisted, updated_at)
|
||||
VALUES (?, NULL, NULL, 0, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(ip) DO UPDATE SET whitelisted=excluded.whitelisted, permanent=CASE WHEN excluded.whitelisted=1 THEN 0 ELSE permanent END, until=CASE WHEN excluded.whitelisted=1 THEN NULL ELSE until END, updated_at=CURRENT_TIMESTAMP`, ip, w)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
|
||||
// helper: turn empty string to sql.NullString
|
||||
func nullIfEmpty(s string) sql.NullString {
|
||||
if strings.TrimSpace(s) == "" { return sql.NullString{Valid: false} }
|
||||
return sql.NullString{String: s, Valid: true}
|
||||
}
|
||||
|
||||
// AdminLogsPage shows recent access, error, failed login, and IP ban entries
|
||||
func (h *Handlers) AdminLogsPage(c *gin.Context) {
|
||||
|
||||
// Parse filters
|
||||
ipFilter := strings.TrimSpace(c.Query("ip"))
|
||||
hoursStr := strings.TrimSpace(c.Query("hours"))
|
||||
limitStr := strings.TrimSpace(c.Query("limit"))
|
||||
bansFilter := strings.TrimSpace(c.Query("bans")) // all|active|perma|whitelist
|
||||
|
||||
// Defaults
|
||||
var lastSince *time.Time
|
||||
if hoursStr != "" {
|
||||
if hInt, err := strconv.Atoi(hoursStr); err == nil && hInt > 0 {
|
||||
t := time.Now().Add(-time.Duration(hInt) * time.Hour)
|
||||
lastSince = &t
|
||||
}
|
||||
}
|
||||
lim := 100
|
||||
if limitStr != "" {
|
||||
if v, err := strconv.Atoi(limitStr); err == nil {
|
||||
if v <= 0 {
|
||||
lim = 100
|
||||
} else if v > 500 {
|
||||
lim = 500
|
||||
} else {
|
||||
lim = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if bansFilter == "" {
|
||||
bansFilter = "all"
|
||||
}
|
||||
|
||||
// Query recent entries with filters
|
||||
type accessRow struct {
|
||||
CreatedAt time.Time
|
||||
IP string
|
||||
Method string
|
||||
Path string
|
||||
Status int
|
||||
Duration int64
|
||||
UserAgent string
|
||||
UserID sql.NullInt64
|
||||
Username sql.NullString
|
||||
}
|
||||
type errorRow struct {
|
||||
CreatedAt time.Time
|
||||
IP sql.NullString
|
||||
Path sql.NullString
|
||||
Message string
|
||||
}
|
||||
type failedRow struct {
|
||||
CreatedAt time.Time
|
||||
IP string
|
||||
Username sql.NullString
|
||||
Type string
|
||||
UserID sql.NullInt64
|
||||
}
|
||||
type banRow struct {
|
||||
IP string
|
||||
Reason sql.NullString
|
||||
Until sql.NullTime
|
||||
Permanent int
|
||||
Whitelisted int
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// Access logs
|
||||
access := []accessRow{}
|
||||
{
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString("SELECT al.created_at, al.ip, al.method, al.path, al.status, al.duration_ms, al.user_agent, al.user_id, u.username FROM access_logs al LEFT JOIN users u ON u.id = al.user_id WHERE 1=1")
|
||||
args := []any{}
|
||||
if lastSince != nil {
|
||||
sb.WriteString(" AND al.created_at >= ?")
|
||||
args = append(args, *lastSince)
|
||||
}
|
||||
if ipFilter != "" {
|
||||
sb.WriteString(" AND al.ip LIKE ?")
|
||||
args = append(args, "%"+ipFilter+"%")
|
||||
}
|
||||
sb.WriteString(" ORDER BY al.created_at DESC LIMIT ")
|
||||
sb.WriteString(strconv.Itoa(lim))
|
||||
if rows, err := h.authSvc.DB.Query(sb.String(), args...); err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var r accessRow
|
||||
if err := rows.Scan(&r.CreatedAt, &r.IP, &r.Method, &r.Path, &r.Status, &r.Duration, &r.UserAgent, &r.UserID, &r.Username); err == nil {
|
||||
access = append(access, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error logs
|
||||
errors := []errorRow{}
|
||||
{
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString("SELECT created_at, ip, path, message FROM error_logs WHERE 1=1")
|
||||
args := []any{}
|
||||
if lastSince != nil {
|
||||
sb.WriteString(" AND created_at >= ?")
|
||||
args = append(args, *lastSince)
|
||||
}
|
||||
if ipFilter != "" {
|
||||
sb.WriteString(" AND ip IS NOT NULL AND ip LIKE ?")
|
||||
args = append(args, "%"+ipFilter+"%")
|
||||
}
|
||||
sb.WriteString(" ORDER BY created_at DESC LIMIT ")
|
||||
sb.WriteString(strconv.Itoa(lim))
|
||||
if rows, err := h.authSvc.DB.Query(sb.String(), args...); err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var r errorRow
|
||||
if err := rows.Scan(&r.CreatedAt, &r.IP, &r.Path, &r.Message); err == nil {
|
||||
errors = append(errors, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Failed logins
|
||||
failed := []failedRow{}
|
||||
{
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString("SELECT created_at, ip, username, type, user_id FROM failed_logins WHERE 1=1")
|
||||
args := []any{}
|
||||
if lastSince != nil {
|
||||
sb.WriteString(" AND created_at >= ?")
|
||||
args = append(args, *lastSince)
|
||||
}
|
||||
if ipFilter != "" {
|
||||
sb.WriteString(" AND ip LIKE ?")
|
||||
args = append(args, "%"+ipFilter+"%")
|
||||
}
|
||||
sb.WriteString(" ORDER BY created_at DESC LIMIT ")
|
||||
sb.WriteString(strconv.Itoa(lim))
|
||||
if rows, err := h.authSvc.DB.Query(sb.String(), args...); err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var r failedRow
|
||||
if err := rows.Scan(&r.CreatedAt, &r.IP, &r.Username, &r.Type, &r.UserID); err == nil {
|
||||
failed = append(failed, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IP bans
|
||||
bans := []banRow{}
|
||||
{
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString("SELECT ip, reason, until, permanent, whitelisted, updated_at FROM ip_bans WHERE 1=1")
|
||||
args := []any{}
|
||||
switch strings.ToLower(bansFilter) {
|
||||
case "perma", "perm", "permanent":
|
||||
sb.WriteString(" AND permanent = 1")
|
||||
case "whitelist", "whitelisted":
|
||||
sb.WriteString(" AND whitelisted = 1")
|
||||
case "active":
|
||||
// Active means currently in effect and not whitelisted
|
||||
sb.WriteString(" AND whitelisted = 0 AND (permanent = 1 OR (until IS NOT NULL AND until > CURRENT_TIMESTAMP))")
|
||||
default:
|
||||
// all
|
||||
}
|
||||
if ipFilter != "" {
|
||||
sb.WriteString(" AND ip LIKE ?")
|
||||
args = append(args, "%"+ipFilter+"%")
|
||||
}
|
||||
sb.WriteString(" ORDER BY updated_at DESC LIMIT ")
|
||||
sb.WriteString(strconv.Itoa(lim))
|
||||
if rows, err := h.authSvc.DB.Query(sb.String(), args...); err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var r banRow
|
||||
if err := rows.Scan(&r.IP, &r.Reason, &r.Until, &r.Permanent, &r.Whitelisted, &r.UpdatedAt); err == nil {
|
||||
bans = append(bans, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "admin_logs", gin.H{
|
||||
"app_name": h.config.AppName,
|
||||
"NoSidebar": true,
|
||||
"breadcrumbs": []gin.H{{"Name": "/", "URL": "/"}, {"Name": "Admin", "URL": "/editor/admin"}, {"Name": "Logs", "URL": ""}},
|
||||
"Page": "admin_logs",
|
||||
"AccessLogs": access,
|
||||
"ErrorLogs": errors,
|
||||
"FailedLogins": failed,
|
||||
"IPBans": bans,
|
||||
// Echo filters back to template
|
||||
"FilterIP": ipFilter,
|
||||
"FilterHours": hoursStr,
|
||||
"FilterLimit": lim,
|
||||
"FilterBans": bansFilter,
|
||||
})
|
||||
}
|
||||
|
||||
// AdminClearAccessLogs clears access logs based on days parameter: 0=all, default 7
|
||||
func (h *Handlers) AdminClearAccessLogs(c *gin.Context) {
|
||||
daysStr := strings.TrimSpace(c.PostForm("days"))
|
||||
days := 7
|
||||
if daysStr != "" {
|
||||
if v, err := strconv.Atoi(daysStr); err == nil && v >= 0 {
|
||||
days = v
|
||||
}
|
||||
}
|
||||
var (
|
||||
query string
|
||||
args []any
|
||||
)
|
||||
if days == 0 {
|
||||
query = `DELETE FROM access_logs`
|
||||
} else {
|
||||
query = `DELETE FROM access_logs WHERE created_at < DATETIME('now', ? || ' days')`
|
||||
// negative days for SQLite modifier, e.g., '-7 days'
|
||||
args = append(args, fmt.Sprintf("-%d", days))
|
||||
}
|
||||
if _, err := h.authSvc.DB.Exec(query, args...); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// ProfilePage renders the user profile page for the signed-in user
|
||||
func (h *Handlers) ProfilePage(c *gin.Context) {
|
||||
// Must be authenticated; middleware ensures user_id is set
|
||||
|
||||
@@ -207,3 +207,75 @@ func boolToStr(b bool) string {
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
// --- Security (IP Ban & Thresholds) Settings ---
|
||||
|
||||
// GetSecuritySettingsHandler returns current security-related config
|
||||
func (h *Handlers) GetSecuritySettingsHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"pwd_failures_threshold": h.config.PwdFailuresThreshold,
|
||||
"mfa_failures_threshold": h.config.MFAFailuresThreshold,
|
||||
"failures_window_minutes": h.config.FailuresWindowMinutes,
|
||||
"auto_ban_duration_hours": h.config.AutoBanDurationHours,
|
||||
"auto_ban_permanent": h.config.AutoBanPermanent,
|
||||
})
|
||||
}
|
||||
|
||||
// PostSecuritySettingsHandler validates and saves security-related config
|
||||
func (h *Handlers) PostSecuritySettingsHandler(c *gin.Context) {
|
||||
pwdStr := strings.TrimSpace(c.PostForm("pwd_failures_threshold"))
|
||||
mfaStr := strings.TrimSpace(c.PostForm("mfa_failures_threshold"))
|
||||
winStr := strings.TrimSpace(c.PostForm("failures_window_minutes"))
|
||||
durStr := strings.TrimSpace(c.PostForm("auto_ban_duration_hours"))
|
||||
permStr := strings.TrimSpace(c.PostForm("auto_ban_permanent"))
|
||||
|
||||
// basic validation
|
||||
if pwdStr == "" || mfaStr == "" || winStr == "" || durStr == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "All numeric fields are required"})
|
||||
return
|
||||
}
|
||||
if _, err := strconv.Atoi(pwdStr); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid password failures threshold"})
|
||||
return
|
||||
}
|
||||
if _, err := strconv.Atoi(mfaStr); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid MFA failures threshold"})
|
||||
return
|
||||
}
|
||||
if _, err := strconv.Atoi(winStr); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid failures window (minutes)"})
|
||||
return
|
||||
}
|
||||
if _, err := strconv.Atoi(durStr); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid auto-ban duration (hours)"})
|
||||
return
|
||||
}
|
||||
|
||||
// normalize perm
|
||||
perm := strings.EqualFold(permStr, "true") || permStr == "1" || strings.EqualFold(permStr, "on")
|
||||
permStr = boolToStr(perm)
|
||||
|
||||
// Save values
|
||||
if err := h.config.SaveSetting("SECURITY", "PWD_FAILURES_THRESHOLD", pwdStr); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save PWD_FAILURES_THRESHOLD"})
|
||||
return
|
||||
}
|
||||
if err := h.config.SaveSetting("SECURITY", "MFA_FAILURES_THRESHOLD", mfaStr); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save MFA_FAILURES_THRESHOLD"})
|
||||
return
|
||||
}
|
||||
if err := h.config.SaveSetting("SECURITY", "FAILURES_WINDOW_MINUTES", winStr); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save FAILURES_WINDOW_MINUTES"})
|
||||
return
|
||||
}
|
||||
if err := h.config.SaveSetting("SECURITY", "AUTO_BAN_DURATION_HOURS", durStr); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save AUTO_BAN_DURATION_HOURS"})
|
||||
return
|
||||
}
|
||||
if err := h.config.SaveSetting("SECURITY", "AUTO_BAN_PERMANENT", permStr); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save AUTO_BAN_PERMANENT"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user