From aaf90b55da6b070b9caa0be28b47b42ed62e97b3 Mon Sep 17 00:00:00 2001 From: H3ALY Date: Tue, 1 Apr 2025 00:05:48 +0100 Subject: [PATCH] Lots of UI and admin changes, need to clean up the three audit log tables and a few other niggles. --- handlers/admin/audit.go | 46 +++++++- helpers/template.go | 8 +- main.go | 1 + middleware/security.go | 2 +- models/audit.go | 14 +++ models/user.go | 5 +- static/css/site.css | 29 ++++- storage/db.go | 17 +++ templates/admin/logs/audit.html | 27 +++++ templates/index.html | 29 ----- templates/layout.html | 183 +++++++++++++++++++++++++++++--- 11 files changed, 309 insertions(+), 52 deletions(-) create mode 100644 models/audit.go create mode 100644 templates/admin/logs/audit.html diff --git a/handlers/admin/audit.go b/handlers/admin/audit.go index b778968..bf0dabe 100644 --- a/handlers/admin/audit.go +++ b/handlers/admin/audit.go @@ -7,6 +7,7 @@ import ( "net/http" "synlotto-website/helpers" "synlotto-website/middleware" + "synlotto-website/models" ) type AdminLogEntry struct { @@ -47,8 +48,51 @@ func AdminAccessLogHandler(db *sql.DB) http.HandlerFunc { tmpl := template.Must(template.New("").Funcs(helpers.TemplateFuncs()).ParseFiles( "templates/layout.html", - "templates/admin/access_log.html", + "templates/admin/logs/access_log.html", )) _ = tmpl.ExecuteTemplate(w, "layout", context) }) } + +func AuditLogHandler(db *sql.DB) http.HandlerFunc { + return middleware.Auth(true)(func(w http.ResponseWriter, r *http.Request) { + context := helpers.TemplateContext(w, r) + + rows, err := db.Query(` + SELECT timestamp, user_id, action, ip, user_agent + FROM audit_log + ORDER BY timestamp DESC + LIMIT 100 + `) + if err != nil { + log.Println("❌ Failed to load audit log:", err) + http.Error(w, "Could not load audit log", http.StatusInternalServerError) + return + } + defer rows.Close() + + var logs []models.AuditEntry + for rows.Next() { + var entry models.AuditEntry + err := rows.Scan(&entry.Timestamp, &entry.UserID, &entry.Action, &entry.IP, &entry.UserAgent) + if err != nil { + log.Println("⚠️ Failed to scan row:", err) + continue + } + logs = append(logs, entry) + } + + context["AuditLogs"] = logs + + tmpl := template.Must(template.New("").Funcs(helpers.TemplateFuncs()).ParseFiles( + "templates/layout.html", + "templates/admin/logs/audit.html", + )) + + err = tmpl.ExecuteTemplate(w, "layout", context) + if err != nil { + log.Println("❌ Failed to render audit page:", err) + http.Error(w, "Template error", http.StatusInternalServerError) + } + }) +} diff --git a/helpers/template.go b/helpers/template.go index ad7c003..ef82ea1 100644 --- a/helpers/template.go +++ b/helpers/template.go @@ -49,20 +49,24 @@ func TemplateContext(w http.ResponseWriter, r *http.Request) map[string]interfac } var currentUser *models.User + var isAdmin bool switch v := session.Values["user_id"].(type) { case int: currentUser = models.GetUserByID(v) case int64: currentUser = models.GetUserByID(int(v)) - default: - currentUser = nil + } + + if currentUser != nil { + isAdmin = currentUser.IsAdmin } return map[string]interface{}{ "CSRFField": csrf.TemplateField(r), "Flash": flash, "User": currentUser, + "IsAdmin": isAdmin, } } diff --git a/main.go b/main.go index 59b8786..7a48a2f 100644 --- a/main.go +++ b/main.go @@ -46,6 +46,7 @@ func main() { func setupAdminRoutes(mux *http.ServeMux, db *sql.DB) { mux.HandleFunc("/admin/access", middleware.AdminOnly(db, admin.AdminAccessLogHandler(db))) + mux.HandleFunc("/admin/audit", middleware.AdminOnly(db, admin.AuditLogHandler(db))) mux.HandleFunc("/admin/dashboard", middleware.AdminOnly(db, admin.AdminDashboardHandler(db))) mux.HandleFunc("/admin/triggers", middleware.AdminOnly(db, admin.AdminTriggersHandler(db))) diff --git a/middleware/security.go b/middleware/security.go index 79bde08..20e8055 100644 --- a/middleware/security.go +++ b/middleware/security.go @@ -15,7 +15,7 @@ func EnforceHTTPS(next http.Handler, enabled bool) http.Handler { func SecureHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Security-Policy", "default-src 'self'") + w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' https://cdn.jsdelivr.net; script-src 'self' https://cdn.jsdelivr.net; font-src 'self' https://cdn.jsdelivr.net") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-XSS-Protection", "1; mode=block") diff --git a/models/audit.go b/models/audit.go new file mode 100644 index 0000000..473d9c0 --- /dev/null +++ b/models/audit.go @@ -0,0 +1,14 @@ +package models + +import "time" + +type AuditEntry struct { + ID int + UserID int + Username string + Action string + Path string + IP string + UserAgent string + Timestamp time.Time +} diff --git a/models/user.go b/models/user.go index 6f64b70..33c940d 100644 --- a/models/user.go +++ b/models/user.go @@ -10,6 +10,7 @@ type User struct { Id int Username string PasswordHash string + IsAdmin bool } var db *sql.DB @@ -40,10 +41,10 @@ func GetUserByUsername(username string) *User { } func GetUserByID(id int) *User { - row := db.QueryRow("SELECT id, username, password_hash FROM users WHERE id = ?", id) + row := db.QueryRow("SELECT id, username, password_hash, is_admin FROM users WHERE id = ?", id) var user User - err := row.Scan(&user.Id, &user.Username, &user.PasswordHash) + err := row.Scan(&user.Id, &user.Username, &user.PasswordHash, &user.IsAdmin) if err != nil { if err != sql.ErrNoRows { log.Println("DB error:", err) diff --git a/static/css/site.css b/static/css/site.css index 23cc4b9..8975d60 100644 --- a/static/css/site.css +++ b/static/css/site.css @@ -1,12 +1,37 @@ -body { font-family: Arial, sans-serif; margin: 40px; } +body { font-family: Arial, sans-serif; margin: 0px; } table { border-collapse: collapse; width: 100%; margin-top: 20px; } th, td { padding: 8px 12px; border: 1px solid #ddd; text-align: center; } th { background-color: #f5f5f5; } .form-section { margin-bottom: 20px; } -.topbar { margin-bottom: 20px; } +.topbar { + display: flex; + justify-content: flex-end; + padding: 0.5rem, 0.5rem, 0.5rem, 0.5rem; + font-size: 14px; +} + +.topbar .auth p { + margin: 0; +} .flash { padding: 10px; color: green; background: #e9ffe9; border: 1px solid #c3e6c3; margin-bottom: 15px; } +.dropdown-admin-box { + min-width: 350px; +} +.dropdown-notification-box { + min-width: 350px; +} + +.dropdown-message-box { + min-width: 350px; +} + +.btn-xs { + padding: 2px 6px; + font-size: 0.75rem; + line-height: 1; +} /* Ball Stuff */ .ball { diff --git a/storage/db.go b/storage/db.go index 05f9bcf..b1c2f60 100644 --- a/storage/db.go +++ b/storage/db.go @@ -184,5 +184,22 @@ func InitDB(filepath string) *sql.DB { if _, err := db.Exec(createAdminAccessLogTable); err != nil { log.Fatal("❌ Failed to create admin access log table:", err) } + + createNewAuditLogTable := ` + CREATE TABLE IF NOT EXISTS audit_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + username TEXT, + action TEXT, + path TEXT, + ip TEXT, + user_agent TEXT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + );` + + if _, err := db.Exec(createNewAuditLogTable); err != nil { + log.Fatal("❌ Failed to create admin access log table:", err) + } + return db } diff --git a/templates/admin/logs/audit.html b/templates/admin/logs/audit.html new file mode 100644 index 0000000..beb874b --- /dev/null +++ b/templates/admin/logs/audit.html @@ -0,0 +1,27 @@ +{{ define "content" }} +

Audit Log

+

Recent sensitive admin events and system activity:

+ + + + + + + + + + + + + {{ range .AuditLogs }} + + + + + + + + {{ end }} + +
TimeUser IDActionIPUser Agent
{{ .Timestamp }}{{ .UserID }}{{ .Action }}{{ .IP }}{{ .UserAgent }}
+{{ end }} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 1b818f2..3485192 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,32 +1,3 @@ {{ define "content" }} -+ Add New Draw - - - - - - - - - - {{ if .Data }} - {{ range .Data }} - - - - - - - - - {{ end }} - {{ else }} - - {{ end }} -
Draw NumberDateMachineBall SetBallsThunderball
{{ .Id }}{{ .DrawDate }}{{ .Machine }}{{ .BallSet }} - {{ range $i, $n := .SortedBalls }} - {{ if $i }}, {{ end }}{{ $n }} - {{ end }} - {{ .Thunderball }}
No draws recorded yet.
{{ end }} diff --git a/templates/layout.html b/templates/layout.html index f33c9d1..e08d539 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -1,27 +1,180 @@ {{ define "layout" }} - - - - SynLotto + + SynLotto + + + - -
+ + + + + - {{ if .Flash }} -
{{ .Flash }}
- {{ end }} + +
+
+ + - {{ template "content" . }} + +
+ {{ if .Flash }} + + {{ end }} + {{ template "content" . }} +
+
+
+ + + + + + {{ end }}