From c3a7480c659e6c33b7c90fc5390a91d61042eebf Mon Sep 17 00:00:00 2001 From: H3ALY Date: Mon, 31 Mar 2025 10:52:12 +0100 Subject: [PATCH] expand on admin functionality, hardening still needs to be implemented. --- handlers/admin/dashboard.go | 68 ++++++++++++++++++ handlers/admin/draws.go | 72 +++++++++++++++++++ handlers/admin/prizes.go | 70 ++++++++++++++++++ handlers/draw_handler.go | 38 ++++++++++ main.go | 40 +++++++---- models/draw.go | 9 +++ models/match.go | 9 +++ templates/admin/dashboard.html | 49 +++++++++++++ templates/admin/draws/delete_draw.html | 9 +++ templates/admin/draws/list_draws.html | 46 ++++++++++++ templates/admin/draws/modify_draw.html | 12 ++++ templates/admin/draws/new_draw.html | 11 +++ templates/admin/draws/prizes/add_prizes.html | 13 ++++ .../admin/draws/prizes/modify_prizes.html | 13 ++++ templates/layout.html | 1 + 15 files changed, 447 insertions(+), 13 deletions(-) create mode 100644 handlers/admin/dashboard.go create mode 100644 handlers/admin/draws.go create mode 100644 handlers/admin/prizes.go create mode 100644 templates/admin/dashboard.html create mode 100644 templates/admin/draws/delete_draw.html create mode 100644 templates/admin/draws/list_draws.html create mode 100644 templates/admin/draws/modify_draw.html create mode 100644 templates/admin/draws/new_draw.html create mode 100644 templates/admin/draws/prizes/add_prizes.html create mode 100644 templates/admin/draws/prizes/modify_prizes.html diff --git a/handlers/admin/dashboard.go b/handlers/admin/dashboard.go new file mode 100644 index 0000000..c3d7549 --- /dev/null +++ b/handlers/admin/dashboard.go @@ -0,0 +1,68 @@ +package handlers + +import ( + "database/sql" + "html/template" + "log" + "net/http" + + helpers "synlotto-website/helpers" + "synlotto-website/models" +) + +func AdminDashboardHandler(db *sql.DB) http.HandlerFunc { + return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { + // userID, ok := helpers.GetCurrentUserID(r) + // if !ok { + // http.Redirect(w, r, "/login", http.StatusSeeOther) + // return + // } + + // TODO: check is_admin from users table here + + context := helpers.TemplateContext(w, r) + + // Total ticket stats + var total, winners int + var prizeSum float64 + db.QueryRow(`SELECT COUNT(*), SUM(CASE WHEN is_winner THEN 1 ELSE 0 END), SUM(prize_amount) FROM my_tickets`).Scan(&total, &winners, &prizeSum) + context["Stats"] = map[string]interface{}{ + "TotalTickets": total, + "TotalWinners": winners, + "TotalPrizeAmount": prizeSum, + } + + // Match run log + rows, err := db.Query(` + SELECT run_at, triggered_by, tickets_matched, winners_found, COALESCE(notes, '') + FROM log_ticket_matching + ORDER BY run_at DESC LIMIT 10 + `) + if err != nil { + log.Println("⚠️ Failed to load logs:", err) + } + defer rows.Close() + + var logs []models.MatchLog + for rows.Next() { + var logEntry models.MatchLog + err := rows.Scan(&logEntry.RunAt, &logEntry.TriggeredBy, &logEntry.TicketsMatched, &logEntry.WinnersFound, &logEntry.Notes) + if err != nil { + log.Println("⚠️ Failed to scan log row:", err) + continue + } + logs = append(logs, logEntry) + } + context["MatchLogs"] = logs + + tmpl := template.Must(template.New("").Funcs(helpers.TemplateFuncs()).ParseFiles( + "templates/layout.html", + "templates/admin/dashboard.html", + )) + + err = tmpl.ExecuteTemplate(w, "layout", context) + if err != nil { + http.Error(w, "Failed to render dashboard", http.StatusInternalServerError) + } + }) +} diff --git a/handlers/admin/draws.go b/handlers/admin/draws.go new file mode 100644 index 0000000..1cbfa0a --- /dev/null +++ b/handlers/admin/draws.go @@ -0,0 +1,72 @@ +package handlers + +import ( + "database/sql" + "html/template" + "net/http" + + helpers "synlotto-website/helpers" +) + +func NewDrawHandler(db *sql.DB) http.HandlerFunc { + return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { + context := helpers.TemplateContext(w, r) + + if r.Method == http.MethodPost { + game := r.FormValue("game_type") + date := r.FormValue("draw_date") + machine := r.FormValue("machine") + ballset := r.FormValue("ball_set") + + _, err := db.Exec(`INSERT INTO results_thunderball (game_type, draw_date, machine, ball_set) VALUES (?, ?, ?, ?)`, + game, date, machine, ballset) + if err != nil { + http.Error(w, "Failed to add draw", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/admin/dashboard", http.StatusSeeOther) + return + } + + tmpl := template.Must(template.New("new_draw").Funcs(helpers.TemplateFuncs()).ParseFiles( + "templates/layout.html", + "templates/admin/draws/new_draw.html", + )) + tmpl.ExecuteTemplate(w, "layout", context) + }) +} + +func ModifyDrawHandler(db *sql.DB) http.HandlerFunc { + return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + id := r.FormValue("id") + _, err := db.Exec(`UPDATE results_thunderball SET game_type=?, draw_date=?, ball_set=?, machine=? WHERE id=?`, + r.FormValue("game_type"), r.FormValue("draw_date"), r.FormValue("ball_set"), r.FormValue("machine"), id) + if err != nil { + http.Error(w, "Update failed", http.StatusInternalServerError) + return + } + http.Redirect(w, r, "/admin/dashboard", http.StatusSeeOther) + return + } + // For GET: load draw by ID (pseudo-code) + // id := r.URL.Query().Get("id") + // query DB, pass into context.Draw + }) +} + +func DeleteDrawHandler(db *sql.DB) http.HandlerFunc { + return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + id := r.FormValue("id") + _, err := db.Exec(`DELETE FROM results_thunderball WHERE id = ?`, id) + if err != nil { + http.Error(w, "Delete failed", http.StatusInternalServerError) + return + } + http.Redirect(w, r, "/admin/dashboard", http.StatusSeeOther) + return + } + }) +} diff --git a/handlers/admin/prizes.go b/handlers/admin/prizes.go new file mode 100644 index 0000000..821807e --- /dev/null +++ b/handlers/admin/prizes.go @@ -0,0 +1,70 @@ +package handlers + +import ( + "database/sql" + "fmt" + "html/template" + "net/http" + "strconv" + "synlotto-website/helpers" +) + +func AddPrizesHandler(db *sql.DB) http.HandlerFunc { + return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + tmpl := template.Must(template.New("").Funcs(helpers.TemplateFuncs()).ParseFiles( + "templates/layout.html", + "templates/admin/draws/prizes/add_prizes.html", + )) + tmpl.ExecuteTemplate(w, "layout", helpers.TemplateContext(w, r)) + return + } + + drawDate := r.FormValue("draw_date") + values := make([]interface{}, 0) + for i := 1; i <= 9; i++ { + val, _ := strconv.Atoi(r.FormValue(fmt.Sprintf("prize%d_per_winner", i))) + values = append(values, val) + } + + stmt := `INSERT INTO prizes_thunderball ( + draw_date, prize1_per_winner, prize2_per_winner, prize3_per_winner, + prize4_per_winner, prize5_per_winner, prize6_per_winner, + prize7_per_winner, prize8_per_winner, prize9_per_winner + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + + _, err := db.Exec(stmt, append([]interface{}{drawDate}, values...)...) + if err != nil { + http.Error(w, "Insert failed: "+err.Error(), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/admin/draws", http.StatusSeeOther) + }) +} + +func ModifyPrizesHandler(db *sql.DB) http.HandlerFunc { + return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + tmpl := template.Must(template.New("").Funcs(helpers.TemplateFuncs()).ParseFiles( + "templates/layout.html", + "templates/admin/draws/prizes/modify_prizes.html", + )) + tmpl.ExecuteTemplate(w, "layout", helpers.TemplateContext(w, r)) + return + } + + drawDate := r.FormValue("draw_date") + for i := 1; i <= 9; i++ { + key := fmt.Sprintf("prize%d_per_winner", i) + val, _ := strconv.Atoi(r.FormValue(key)) + _, err := db.Exec("UPDATE prizes_thunderball SET "+key+" = ? WHERE draw_date = ?", val, drawDate) + if err != nil { + http.Error(w, "Update failed: "+err.Error(), http.StatusInternalServerError) + return + } + } + + http.Redirect(w, r, "/admin/draws", http.StatusSeeOther) + }) +} diff --git a/handlers/draw_handler.go b/handlers/draw_handler.go index 99c3221..0138b48 100644 --- a/handlers/draw_handler.go +++ b/handlers/draw_handler.go @@ -107,3 +107,41 @@ func Submit(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusSeeOther) } + +func ListDrawsHandler(db *sql.DB) http.HandlerFunc { + return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { + context := helpers.TemplateContext(w, r) + draws := []models.DrawSummary{} + + rows, err := db.Query(` + SELECT r.id, r.game_type, r.draw_date, r.ball_set, r.machine, + (SELECT COUNT(1) FROM prizes_thunderball p WHERE p.draw_date = r.draw_date) as prize_exists + FROM results_thunderball r + ORDER BY r.draw_date DESC + `) + if err != nil { + http.Error(w, "Failed to query draws", http.StatusInternalServerError) + return + } + defer rows.Close() + + for rows.Next() { + var d models.DrawSummary + var prizeFlag int + if err := rows.Scan(&d.Id, &d.GameType, &d.DrawDate, &d.BallSet, &d.Machine, &prizeFlag); err != nil { + log.Println("⚠️ Draw scan failed:", err) + continue + } + d.PrizeSet = prizeFlag > 0 + draws = append(draws, d) + } + + context["Draws"] = draws + + tmpl := template.Must(template.New("draw_list").Funcs(helpers.TemplateFuncs()).ParseFiles( + "templates/layout.html", + "templates/admin/draws/list.html", + )) + tmpl.ExecuteTemplate(w, "layout", context) + }) +} diff --git a/main.go b/main.go index d6ba515..7b59cdf 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,11 @@ package main import ( + "database/sql" "log" "net/http" "synlotto-website/handlers" - services "synlotto-website/handlers/admin" + admin "synlotto-website/handlers/admin" "synlotto-website/helpers" "synlotto-website/middleware" "synlotto-website/models" @@ -24,27 +25,40 @@ func main() { ) mux := http.NewServeMux() + setupAdminRoutes(mux, db) + setupAccountRoutes(mux, db) + setupResultRoutes(mux, db) - // Styling mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) mux.HandleFunc("/", handlers.Home(db)) - mux.HandleFunc("/new", handlers.NewDraw) // ToDo: needs to be wrapped in admin auth - mux.HandleFunc("/submit", handlers.Submit) - // Result pages - mux.HandleFunc("/results/thunderball", handlers.ResultsThunderball(db)) + log.Println("🌐 Running on http://localhost:8080") + http.ListenAndServe(":8080", helpers.RateLimit(csrfMiddleware(mux))) +} - // Account Pages +func setupAdminRoutes(mux *http.ServeMux, db *sql.DB) { + mux.HandleFunc("/admin/dashboard", admin.AdminDashboardHandler(db)) + mux.HandleFunc("/admin/triggers", admin.AdminTriggersHandler(db)) + + // Draw management + mux.HandleFunc("/admin/draws/new", admin.NewDrawHandler(db)) + mux.HandleFunc("/admin/draws/modify", admin.ModifyDrawHandler(db)) + mux.HandleFunc("/admin/draws/delete", admin.DeleteDrawHandler(db)) + + // Prize management + mux.HandleFunc("/admin/draws/prizes/add", admin.AddPrizesHandler(db)) + mux.HandleFunc("/admin/draws/prizes/modify", admin.ModifyPrizesHandler(db)) +} + +func setupAccountRoutes(mux *http.ServeMux, db *sql.DB) { mux.HandleFunc("/login", middleware.Auth(false)(handlers.Login)) mux.HandleFunc("/logout", handlers.Logout) mux.HandleFunc("/signup", middleware.Auth(false)(handlers.Signup)) mux.HandleFunc("/account/tickets/add_ticket", handlers.AddTicket(db)) mux.HandleFunc("/account/tickets/my_tickets", handlers.GetMyTickets(db)) - - // Admin Pages - mux.HandleFunc("/admin/triggers", services.AdminTriggersHandler(db)) - - log.Println("🌐 Running on http://localhost:8080") - http.ListenAndServe(":8080", helpers.RateLimit(csrfMiddleware(mux))) +} + +func setupResultRoutes(mux *http.ServeMux, db *sql.DB) { + mux.HandleFunc("/results/thunderball", handlers.ResultsThunderball(db)) } diff --git a/models/draw.go b/models/draw.go index c542f44..f22d79d 100644 --- a/models/draw.go +++ b/models/draw.go @@ -1,5 +1,14 @@ package models +type DrawSummary struct { + Id int + GameType string + DrawDate string + BallSet string + Machine string + PrizeSet bool +} + type ThunderballResult struct { Id int DrawDate string diff --git a/models/match.go b/models/match.go index 01e7f8a..77b33c2 100644 --- a/models/match.go +++ b/models/match.go @@ -38,3 +38,12 @@ type MatchRunStats struct { TicketsMatched int WinnersFound int } + +type MatchLog struct { + ID int + TriggeredBy string + RunAt string + TicketsMatched int + WinnersFound int + Notes string +} diff --git a/templates/admin/dashboard.html b/templates/admin/dashboard.html new file mode 100644 index 0000000..8396821 --- /dev/null +++ b/templates/admin/dashboard.html @@ -0,0 +1,49 @@ +{{ define "content" }} +

📊 Admin Dashboard

+

Welcome back, admin.

+ +
+
+

Total Tickets

+

{{ .Stats.TotalTickets }}

+
+
+

Total Winners

+

{{ .Stats.TotalWinners }}

+
+
+

Prize Fund Awarded

+

£{{ printf "%.2f" .Stats.TotalPrizeAmount }}

+
+
+ +
+

Recent Ticket Matches

+ + + + + + + + + + + + {{ range .MatchLogs }} + + + + + + + + {{ else }} + + + + {{ end }} + +
Draw DateTriggered ByMatchedWinnersNotes
{{ .RunAt }}{{ .TriggeredBy }}{{ .TicketsMatched }}{{ .WinnersFound }}{{ .Notes }}
No match history found
+
+{{ end }} diff --git a/templates/admin/draws/delete_draw.html b/templates/admin/draws/delete_draw.html new file mode 100644 index 0000000..c4548e7 --- /dev/null +++ b/templates/admin/draws/delete_draw.html @@ -0,0 +1,9 @@ +{{ define "delete_draw" }} +

Delete Draw

+
+ {{ .CSRFField }} + +

Are you sure you want to delete the draw on {{ .Draw.DrawDate }}?

+ +
+{{ end }} \ No newline at end of file diff --git a/templates/admin/draws/list_draws.html b/templates/admin/draws/list_draws.html new file mode 100644 index 0000000..6bc6dcb --- /dev/null +++ b/templates/admin/draws/list_draws.html @@ -0,0 +1,46 @@ +{{ define "draw_list" }} +

Draws Overview

+ + + + + + + + + + + + + + + {{ range .Draws }} + + + + + + + + + + {{ else }} + + {{ end }} + +
IDGameDateBall SetMachinePrizes?Actions
{{ .ID }}{{ .GameType }}{{ .DrawDate }}{{ .BallSet }}{{ .Machine }} + {{ if .PrizeSet }} + ✅ + {{ else }} + ❌ + {{ end }} + + Edit | + Delete | + {{ if .PrizeSet }} + Modify Prizes + {{ else }} + Add Prizes + {{ end }} +
No draws available.
+{{ end }} diff --git a/templates/admin/draws/modify_draw.html b/templates/admin/draws/modify_draw.html new file mode 100644 index 0000000..7703de0 --- /dev/null +++ b/templates/admin/draws/modify_draw.html @@ -0,0 +1,12 @@ +{{ define "modify_draw" }} +

Modify Draw

+
+ {{ .CSRFField }} + + + + + + +
+{{ end }} \ No newline at end of file diff --git a/templates/admin/draws/new_draw.html b/templates/admin/draws/new_draw.html new file mode 100644 index 0000000..19b653a --- /dev/null +++ b/templates/admin/draws/new_draw.html @@ -0,0 +1,11 @@ +{{ define "new_draw" }} +

Add New Draw

+
+ {{ .CSRFField }} + + + + + +
+{{ end }} \ No newline at end of file diff --git a/templates/admin/draws/prizes/add_prizes.html b/templates/admin/draws/prizes/add_prizes.html new file mode 100644 index 0000000..2bbf7d9 --- /dev/null +++ b/templates/admin/draws/prizes/add_prizes.html @@ -0,0 +1,13 @@ +{{ define "add_prizes" }} +

Add Prize Breakdown

+
+ {{ .CSRFField }} + + {{ range $i, $ := .PrizeLabels }} + + {{ end }} + +
+{{ end }} \ No newline at end of file diff --git a/templates/admin/draws/prizes/modify_prizes.html b/templates/admin/draws/prizes/modify_prizes.html new file mode 100644 index 0000000..1775df3 --- /dev/null +++ b/templates/admin/draws/prizes/modify_prizes.html @@ -0,0 +1,13 @@ +{{ define "modify_prizes" }} +

Modify Prize Breakdown

+
+ {{ .CSRFField }} + + {{ range $i, $ := .PrizeLabels }} + + {{ end }} + +
+{{ end }} diff --git a/templates/layout.html b/templates/layout.html index 53d2aaa..f33c9d1 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -15,6 +15,7 @@

Login

{{ end }} + Dashboard {{ if .Flash }}
{{ .Flash }}