Lots of UI and admin changes, need to clean up the three audit log tables and a few other niggles.

This commit is contained in:
2025-04-01 00:05:48 +01:00
parent 7eefb9ced0
commit aaf90b55da
11 changed files with 309 additions and 52 deletions

View File

@@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"synlotto-website/helpers" "synlotto-website/helpers"
"synlotto-website/middleware" "synlotto-website/middleware"
"synlotto-website/models"
) )
type AdminLogEntry struct { type AdminLogEntry struct {
@@ -47,8 +48,51 @@ func AdminAccessLogHandler(db *sql.DB) http.HandlerFunc {
tmpl := template.Must(template.New("").Funcs(helpers.TemplateFuncs()).ParseFiles( tmpl := template.Must(template.New("").Funcs(helpers.TemplateFuncs()).ParseFiles(
"templates/layout.html", "templates/layout.html",
"templates/admin/access_log.html", "templates/admin/logs/access_log.html",
)) ))
_ = tmpl.ExecuteTemplate(w, "layout", context) _ = 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)
}
})
}

View File

@@ -49,20 +49,24 @@ func TemplateContext(w http.ResponseWriter, r *http.Request) map[string]interfac
} }
var currentUser *models.User var currentUser *models.User
var isAdmin bool
switch v := session.Values["user_id"].(type) { switch v := session.Values["user_id"].(type) {
case int: case int:
currentUser = models.GetUserByID(v) currentUser = models.GetUserByID(v)
case int64: case int64:
currentUser = models.GetUserByID(int(v)) currentUser = models.GetUserByID(int(v))
default: }
currentUser = nil
if currentUser != nil {
isAdmin = currentUser.IsAdmin
} }
return map[string]interface{}{ return map[string]interface{}{
"CSRFField": csrf.TemplateField(r), "CSRFField": csrf.TemplateField(r),
"Flash": flash, "Flash": flash,
"User": currentUser, "User": currentUser,
"IsAdmin": isAdmin,
} }
} }

View File

@@ -46,6 +46,7 @@ func main() {
func setupAdminRoutes(mux *http.ServeMux, db *sql.DB) { func setupAdminRoutes(mux *http.ServeMux, db *sql.DB) {
mux.HandleFunc("/admin/access", middleware.AdminOnly(db, admin.AdminAccessLogHandler(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/dashboard", middleware.AdminOnly(db, admin.AdminDashboardHandler(db)))
mux.HandleFunc("/admin/triggers", middleware.AdminOnly(db, admin.AdminTriggersHandler(db))) mux.HandleFunc("/admin/triggers", middleware.AdminOnly(db, admin.AdminTriggersHandler(db)))

View File

@@ -15,7 +15,7 @@ func EnforceHTTPS(next http.Handler, enabled bool) http.Handler {
func SecureHeaders(next http.Handler) http.Handler { func SecureHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block") w.Header().Set("X-XSS-Protection", "1; mode=block")

14
models/audit.go Normal file
View File

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

View File

@@ -10,6 +10,7 @@ type User struct {
Id int Id int
Username string Username string
PasswordHash string PasswordHash string
IsAdmin bool
} }
var db *sql.DB var db *sql.DB
@@ -40,10 +41,10 @@ func GetUserByUsername(username string) *User {
} }
func GetUserByID(id int) *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 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 != nil {
if err != sql.ErrNoRows { if err != sql.ErrNoRows {
log.Println("DB error:", err) log.Println("DB error:", err)

View File

@@ -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; } table { border-collapse: collapse; width: 100%; margin-top: 20px; }
th, td { padding: 8px 12px; border: 1px solid #ddd; text-align: center; } th, td { padding: 8px 12px; border: 1px solid #ddd; text-align: center; }
th { background-color: #f5f5f5; } th { background-color: #f5f5f5; }
.form-section { margin-bottom: 20px; } .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; } .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 Stuff */
.ball { .ball {

View File

@@ -184,5 +184,22 @@ func InitDB(filepath string) *sql.DB {
if _, err := db.Exec(createAdminAccessLogTable); err != nil { if _, err := db.Exec(createAdminAccessLogTable); err != nil {
log.Fatal("❌ Failed to create admin access log table:", err) 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 return db
} }

View File

@@ -0,0 +1,27 @@
{{ define "content" }}
<h2>Audit Log</h2>
<p>Recent sensitive admin events and system activity:</p>
<table>
<thead>
<tr>
<th>Time</th>
<th>User ID</th>
<th>Action</th>
<th>IP</th>
<th>User Agent</th>
</tr>
</thead>
<tbody>
{{ range .AuditLogs }}
<tr>
<td>{{ .Timestamp }}</td>
<td>{{ .UserID }}</td>
<td>{{ .Action }}</td>
<td>{{ .IP }}</td>
<td>{{ .UserAgent }}</td>
</tr>
{{ end }}
</tbody>
</table>
{{ end }}

View File

@@ -1,32 +1,3 @@
{{ define "content" }} {{ define "content" }}
<a href="/new">+ Add New Draw</a>
<table>
<tr>
<th>Draw Number</th>
<th>Date</th>
<th>Machine</th>
<th>Ball Set</th>
<th>Balls</th>
<th>Thunderball</th>
</tr>
{{ if .Data }}
{{ range .Data }}
<tr>
<td>{{ .Id }}</td>
<td>{{ .DrawDate }}</td>
<td>{{ .Machine }}</td>
<td>{{ .BallSet }}</td>
<td>
{{ range $i, $n := .SortedBalls }}
{{ if $i }}, {{ end }}{{ $n }}
{{ end }}
</td>
<td>{{ .Thunderball }}</td>
</tr>
{{ end }}
{{ else }}
<tr><td colspan="5">No draws recorded yet.</td></tr>
{{ end }}
</table>
{{ end }} {{ end }}

View File

@@ -1,27 +1,180 @@
{{ define "layout" }} {{ define "layout" }}
<!DOCTYPE html> <!DOCTYPE html>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@3.3.5/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="/static/css/site.css">
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>SynLotto</title> <title>SynLotto</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<link rel="stylesheet" href="/static/css/site.css">
</head> </head>
<body>
<div class="topbar"> <body class="d-flex flex-column min-vh-100">
<!-- Topbar -->
<nav class="navbar navbar-expand-lg navbar-light bg-light px-3">
<a class="navbar-brand d-flex align-items-center" href="/">
<img src="/static/img/logo.png" alt="Logo" height="30" class="me-2">
<span>SynLotto</span>
</a>
<div class="ms-auto d-flex align-items-center gap-3">
{{ if .User }} {{ if .User }}
<p>Hello, {{ .User.Username }} | <a href="/logout">Logout</a></p> {{ if .IsAdmin }}
{{ else }} <!-- Admin Dropdown -->
<p><a href="/login">Login</a></p> <div class="dropdown">
{{ end }} <a class="nav-link text-dark" href="#" id="adminDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-shield-lock fs-5 position-relative"></i>
</a>
<ul class="dropdown-menu dropdown-menu-end dropdown-admin-box shadow-sm" aria-labelledby="adminDropdown">
<li class="dropdown-header text-center fw-bold">Admin Menu</li>
<li><hr class="dropdown-divider"></li>
<li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Tools</a></li>
<li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Audit Logs</a></li>
<li><hr class="dropdown-divider"></li>
<li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Open Dashboard</a></li>
</ul>
</div>
{{ end }}
<!-- Notification Dropdown -->
<div class="dropdown">
<a class="nav-link text-dark" href="#" id="notificationDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-bell fs-5 position-relative">
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-warning text-dark" style="transform: translate(-40%, -50%)">
3
</span>
</i>
</a>
<ul class="dropdown-menu dropdown-menu-end dropdown-notification-box shadow-sm" aria-labelledby="notificationDropdown">
<li class="dropdown-header text-center fw-bold">Notifications</li>
<li><hr class="dropdown-divider"></li>
<!-- Example notification -->
<li class="px-3 py-2">
<div class="d-flex align-items-start">
<i class="bi bi-info-circle text-primary me-2 fs-4"></i>
<div>
<div class="fw-semibold">System Update</div>
<small class="text-muted">A new lottery draw has been posted.</small>
</div>
</div>
</li>
<li><hr class="dropdown-divider"></li>
<li class="px-3 py-2">
<div class="d-flex align-items-start">
<i class="bi bi-check-circle text-success me-2 fs-4"></i>
<div>
<div class="fw-semibold">Sync Complete</div>
<small class="text-muted">All results are up-to-date.</small>
</div>
</div>
</li>
<li><hr class="dropdown-divider"></li>
<li class="text-center"><a href="/notifications" class="dropdown-item">View all notifications</a></li>
</ul>
</div>
<!-- Message Dropdown -->
<div class="dropdown">
<a class="nav-link text-dark" href="#" id="messageDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-envelope fs-5 position-relative">
<!-- Unread badge (example: 2 messages) -->
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-danger text-dark" style="transform: translate(-40%, -50%)">
2
</span>
</i>
</a>
<ul class="dropdown-menu dropdown-menu-end dropdown-message-box shadow-sm" aria-labelledby="messageDropdown" style="min-width: 300px;">
<li class="dropdown-header text-center fw-bold">Messages</li>
<li><hr class="dropdown-divider"></li>
<!-- Example message item -->
<li class="px-3 py-2">
<div class="d-flex align-items-start">
<i class="bi bi-person-circle me-2 fs-4 text-secondary"></i>
<div>
<div class="fw-semibold">Admin</div>
<small class="text-muted">Welcome to SynLotto!</small>
</div>
</div>
</li>
<li><hr class="dropdown-divider"></li>
<li class="text-center"><a href="/messages" class="dropdown-item">View all messages</a></li>
</ul>
</div>
<!-- User Greeting -->
<span class="navbar-text">Hello, {{ .User.Username }}</span>
<a class="btn btn-outline-danger btn-xs" href="/logout">Logout</a>
{{ else }}
<a class="btn btn-outline-primary btn-sm" href="/login">Login</a>
{{ end }}
</div> </div>
<a href="/admin/dashboard" class="hover:underline">Dashboard</a> </nav>
{{ if .Flash }} <!-- Main Layout -->
<div class="flash">{{ .Flash }}</div> <div class="container-fluid flex-grow-1">
{{ end }} <div class="row">
<!-- Sidebar -->
<nav class="col-md-2 d-none d-md-block bg-light sidebar pt-3">
<div class="position-sticky">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link d-flex justify-content-between align-items-center" data-bs-toggle="collapse" href="#lotterySubmenu" role="button" aria-expanded="false" aria-controls="lotterySubmenu">
<strong>Lottery Results</strong>
<i class="bi bi-chevron-down small"></i>
</a>
<div class="collapse ps-3" id="lotterySubmenu">
<ul class="nav flex-column">
<li class="nav-item"><a class="nav-link" href="/lottery/today">Today's Results</a></li>
<li class="nav-item"><a class="nav-link" href="/results/lotto">Lotto</a></li>
<li class="nav-item"><a class="nav-link" href="/results/thunderball">Thunderball</a></li>
<li class="nav-item"><a class="nav-link" href="/results/euromillions">EuroMillions</a></li>
<li class="nav-item"><a class="nav-link" href="/results/Set For Life">Set For Life</a></li>
</ul>
</div>
</li>
<li class="nav-item">
<a class="nav-link d-flex justify-content-between align-items-center" data-bs-toggle="collapse" href="#statisticsSubmenu" role="button" aria-expanded="false" aria-controls="statisticsSubmenu">
<strong>Statistics</strong>
</a>
<div class="collapse ps-3" id="statisticsSubmenu">
<ul class="nav flex-column">
<li class="nav-item"><a class="nav-link" href="/lottery/today">Today's Results</a></li>
<li class="nav-item"><a class="nav-link" href="/lottery/history">Lotto</a></li>
<li class="nav-item"><a class="nav-link" href="/lottery/stats">Thunderball</a></li>
<li class="nav-item"><a class="nav-link" href="/lottery/history">Set For Life</a></li>
<li class="nav-item"><a class="nav-link" href="/lottery/stats">EuroMillions</a></li>
</ul>
</div>
</li>
<li class="nav-item"><strong class="nav-link">Syndicate</strong></li>
</ul>
</div>
</nav>
{{ template "content" . }} <!-- Main Content -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 pt-4">
{{ if .Flash }}
<div class="alert alert-info" role="alert">
{{ .Flash }}
</div>
{{ end }}
{{ template "content" . }}
</main>
</div>
</div>
<!-- Footer -->
<footer class="bg-light text-center text-muted py-3 mt-auto border-top">
<small>&copy; xxx SynLotto. All rights reserved. | <a href="/privacy">Privacy Policy</a> | <a href="/privacy">Terms & Conditions</a> | <a href="/privacy">Contact Us </a></small>
</footer>
<!-- JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body> </body>
</html> </html>
{{ end }} {{ end }}