add loger, access log and bans/whitelist

This commit is contained in:
nahakubuilde
2025-08-26 07:46:01 +01:00
parent e21a0b5b10
commit 4cafd9848f
10 changed files with 1163 additions and 60 deletions

View File

@@ -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