add loger, access log and bans/whitelist
This commit is contained in:
@@ -2,8 +2,10 @@ package server
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -105,3 +107,84 @@ func (s *Server) RequireAdmin() gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// AccessLogger logs all requests to the access_logs table after handling.
|
||||
func (s *Server) AccessLogger() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
c.Next()
|
||||
|
||||
duration := time.Since(start)
|
||||
status := c.Writer.Status()
|
||||
ip := c.ClientIP()
|
||||
method := c.Request.Method
|
||||
path := c.FullPath()
|
||||
if path == "" {
|
||||
path = c.Request.URL.Path
|
||||
}
|
||||
ua := c.Request.UserAgent()
|
||||
|
||||
var userID interface{}
|
||||
if uidAny, ok := c.Get("user_id"); ok {
|
||||
if v, ok := uidAny.(int64); ok {
|
||||
userID = v
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = s.auth.DB.Exec(
|
||||
`INSERT INTO access_logs (user_id, ip, method, path, status, duration_ms, user_agent) VALUES (?,?,?,?,?,?,?)`,
|
||||
userID, ip, method, path, status, duration.Milliseconds(), ua,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// IPBanEnforce blocks banned IPs (unless whitelisted).
|
||||
func (s *Server) IPBanEnforce() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ip := c.ClientIP()
|
||||
|
||||
var whitelisted, permanent int
|
||||
var until sql.NullTime
|
||||
err := s.auth.DB.QueryRow(`SELECT whitelisted, permanent, until FROM ip_bans WHERE ip = ?`, ip).Scan(&whitelisted, &permanent, &until)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
s.logError(c, "ip_ban_lookup_failed: "+err.Error(), "")
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if whitelisted == 1 {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
banned := false
|
||||
if permanent == 1 {
|
||||
banned = true
|
||||
} else if until.Valid && until.Time.After(time.Now()) {
|
||||
banned = true
|
||||
}
|
||||
|
||||
if banned {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "access denied"})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// logError stores error logs (best-effort).
|
||||
func (s *Server) logError(c *gin.Context, message, stack string) {
|
||||
var userID interface{}
|
||||
if uidAny, ok := c.Get("user_id"); ok {
|
||||
if v, ok := uidAny.(int64); ok {
|
||||
userID = v
|
||||
}
|
||||
}
|
||||
ip := c.ClientIP()
|
||||
path := c.Request.URL.Path
|
||||
_, _ = s.auth.DB.Exec(`INSERT INTO error_logs (user_id, ip, path, message, stack) VALUES (?,?,?,?,?)`, userID, ip, path, message, stack)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/sessions"
|
||||
@@ -43,8 +44,11 @@ func New(cfg *config.Config) *Server {
|
||||
auth: authSvc,
|
||||
}
|
||||
|
||||
// Global middlewares: session user + template setup
|
||||
// Global middlewares
|
||||
s.router.Use(s.SessionUser())
|
||||
// Enforce IP bans/whitelists and log access for every request
|
||||
s.router.Use(s.IPBanEnforce())
|
||||
s.router.Use(s.AccessLogger())
|
||||
|
||||
s.setupRoutes()
|
||||
s.setupStaticFiles()
|
||||
@@ -66,6 +70,9 @@ func (s *Server) Start() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Start background cleanup for access logs older than 7 days (daily)
|
||||
go s.startAccessLogCleanup()
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", s.config.Host, s.config.Port)
|
||||
fmt.Printf("Starting Gobsidian server on %s\n", addr)
|
||||
fmt.Printf("Notes directory: %s\n", s.config.NotesDir)
|
||||
@@ -119,6 +126,9 @@ func (s *Server) setupRoutes() {
|
||||
editor.POST("/settings/notes_dir", h.PostNotesDirSettingsHandler)
|
||||
editor.GET("/settings/file_extensions", h.GetFileExtensionsSettingsHandler)
|
||||
editor.POST("/settings/file_extensions", h.PostFileExtensionsSettingsHandler)
|
||||
// Security settings (IP ban thresholds/duration/permanent)
|
||||
editor.GET("/settings/security", h.GetSecuritySettingsHandler)
|
||||
editor.POST("/settings/security", h.PostSecuritySettingsHandler)
|
||||
|
||||
// Profile
|
||||
editor.GET("/profile", h.ProfilePage)
|
||||
@@ -136,6 +146,14 @@ func (s *Server) setupRoutes() {
|
||||
// Admin CRUD API under /editor/admin
|
||||
admin := editor.Group("/admin", s.RequireAdmin())
|
||||
{
|
||||
// Logs page
|
||||
admin.GET("/logs", h.AdminLogsPage)
|
||||
// Manual clear old access logs (older than 7 days)
|
||||
admin.POST("/logs/clear_access", h.AdminClearAccessLogs)
|
||||
// Security: IP ban/whitelist actions
|
||||
admin.POST("/ip/ban", h.AdminBanIP)
|
||||
admin.POST("/ip/unban", h.AdminUnbanIP)
|
||||
admin.POST("/ip/whitelist", h.AdminWhitelistIP)
|
||||
admin.POST("/users", h.AdminCreateUser)
|
||||
admin.DELETE("/users/:id", h.AdminDeleteUser)
|
||||
admin.POST("/users/:id/active", h.AdminSetUserActive)
|
||||
@@ -232,3 +250,13 @@ func (s *Server) setupTemplates() {
|
||||
|
||||
fmt.Printf("DEBUG: Templates loaded successfully\n")
|
||||
}
|
||||
|
||||
// startAccessLogCleanup deletes access logs older than 7 days once at startup and then daily.
|
||||
func (s *Server) startAccessLogCleanup() {
|
||||
// initial cleanup
|
||||
_, _ = s.auth.DB.Exec(`DELETE FROM access_logs WHERE created_at < DATETIME('now', '-7 days')`)
|
||||
ticker := time.NewTicker(24 * time.Hour)
|
||||
for range ticker.C {
|
||||
_, _ = s.auth.DB.Exec(`DELETE FROM access_logs WHERE created_at < DATETIME('now', '-7 days')`)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user