diff --git a/go.mod b/go.mod index b42a2ba..4a63dda 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ require ( github.com/gorilla/csrf v1.7.2 github.com/gorilla/sessions v1.4.0 golang.org/x/crypto v0.36.0 - modernc.org/sqlite v1.36.1 golang.org/x/time v0.11.0 + modernc.org/sqlite v1.36.1 ) require ( diff --git a/handlers/home.go b/handlers/home.go new file mode 100644 index 0000000..44105f1 --- /dev/null +++ b/handlers/home.go @@ -0,0 +1,64 @@ +package handlers + +import ( + "database/sql" + "html/template" + "log" + "net/http" + "sort" + "synlotto-website/helpers" + "synlotto-website/models" +) + +// Home shows latest Thunderball results +func Home(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rows, err := db.Query(` + SELECT id, draw_date, machine, ballset, ball1, ball2, ball3, ball4, ball5, thunderball + FROM results_thunderball + ORDER BY id DESC + `) + if err != nil { + log.Println("❌ DB error:", err) + http.Error(w, "Database error", http.StatusInternalServerError) + return + } + defer rows.Close() + + var results []models.ThunderballResult + + for rows.Next() { + var res models.ThunderballResult + err := rows.Scan( + &res.Id, &res.DrawDate, &res.Machine, &res.BallSet, + &res.Ball1, &res.Ball2, &res.Ball3, &res.Ball4, &res.Ball5, &res.Thunderball, + ) + if err != nil { + log.Println("❌ Row scan error:", err) + continue + } + + res.SortedBalls = []int{ + res.Ball1, res.Ball2, res.Ball3, res.Ball4, res.Ball5, + } + sort.Ints(res.SortedBalls) + + results = append(results, res) + } + + context := BuildTemplateContext(db, w, r) + context["Data"] = results + + tmpl := template.Must(template.New("").Funcs(helpers.TemplateFuncs()).ParseFiles( + "templates/layout.html", + "templates/topbar.html", + "templates/index.html", + )) + + err = tmpl.ExecuteTemplate(w, "layout", context) + if err != nil { + log.Println("❌ Template error:", err) + http.Error(w, "Error rendering homepage", http.StatusInternalServerError) + } + } +} diff --git a/handlers/template_context.go b/handlers/template_context.go new file mode 100644 index 0000000..883a31d --- /dev/null +++ b/handlers/template_context.go @@ -0,0 +1,60 @@ +package handlers + +import ( + "database/sql" + "net/http" + "synlotto-website/helpers" + "synlotto-website/models" + "synlotto-website/storage" + + "github.com/gorilla/csrf" +) + +type TemplateData map[string]interface{} + +func BuildTemplateContext(db *sql.DB, w http.ResponseWriter, r *http.Request) TemplateData { + session, _ := helpers.GetSession(w, r) + + var flash string + if f, ok := session.Values["flash"].(string); ok { + flash = f + delete(session.Values, "flash") + session.Save(r, w) + } + + var currentUser *models.User + var isAdmin bool + + notificationCount := 0 + notifications := []models.Notification{} + messageCount := 0 + messages := []models.Message{} + + switch v := session.Values["user_id"].(type) { + case int: + currentUser = models.GetUserByID(v) + case int64: + currentUser = models.GetUserByID(int(v)) + } + + if currentUser != nil { + isAdmin = currentUser.IsAdmin + + notificationCount = storage.GetNotificationCount(db, currentUser.Id) + notifications = storage.GetRecentNotifications(db, currentUser.Id, 15) + + messageCount, _ = storage.GetMessageCount(db, currentUser.Id) + messages = storage.GetRecentMessages(db, currentUser.Id, 15) + } + + return TemplateData{ + "CSRFField": csrf.TemplateField(r), + "Flash": flash, + "User": currentUser, + "IsAdmin": isAdmin, + "NotificationCount": notificationCount, + "Notifications": notifications, + "MessageCount": messageCount, + "Messages": messages, + } +} diff --git a/helpers/template.go b/helpers/template.go index ef82ea1..5cf79ea 100644 --- a/helpers/template.go +++ b/helpers/template.go @@ -5,6 +5,7 @@ import ( "net/http" "strings" "synlotto-website/models" + "synlotto-website/storage" "github.com/gorilla/csrf" ) @@ -50,6 +51,8 @@ func TemplateContext(w http.ResponseWriter, r *http.Request) map[string]interfac var currentUser *models.User var isAdmin bool + var notificationCount int + var notifications []models.Notification switch v := session.Values["user_id"].(type) { case int: @@ -60,13 +63,17 @@ func TemplateContext(w http.ResponseWriter, r *http.Request) map[string]interfac if currentUser != nil { isAdmin = currentUser.IsAdmin + notificationCount = storage.GetNotificationCount(currentUser.Id) + notifications = storage.GetRecentNotifications(currentUser.Id, 15) } return map[string]interface{}{ - "CSRFField": csrf.TemplateField(r), - "Flash": flash, - "User": currentUser, - "IsAdmin": isAdmin, + "CSRFField": csrf.TemplateField(r), + "Flash": flash, + "User": currentUser, + "IsAdmin": isAdmin, + "NotificationCount": notificationCount, + "Notifications": notifications, } } @@ -100,3 +107,9 @@ func rangeClass(n int) string { return "50-plus" } } + +func SetFlash(w http.ResponseWriter, r *http.Request, message string) { + session, _ := GetSession(w, r) + session.Values["flash"] = message + session.Save(r, w) +} diff --git a/main.go b/main.go index 7a48a2f..f5c50be 100644 --- a/main.go +++ b/main.go @@ -51,7 +51,9 @@ func setupAdminRoutes(mux *http.ServeMux, db *sql.DB) { mux.HandleFunc("/admin/triggers", middleware.AdminOnly(db, admin.AdminTriggersHandler(db))) // Draw management - mux.HandleFunc("/admin/draws/new", middleware.AdminOnly(db, admin.NewDrawHandler(db))) + mux.HandleFunc("/admin/draws", middleware.AdminOnly(db, admin.ListDrawsHandler(db))) + mux.HandleFunc("/admin/draws/new", middleware.AdminOnly(db, admin.RenderNewDrawForm(db))) + mux.HandleFunc("/admin/draws/submit", middleware.AdminOnly(db, admin.CreateDrawHandler(db))) mux.HandleFunc("/admin/draws/modify", middleware.AdminOnly(db, admin.ModifyDrawHandler(db))) mux.HandleFunc("/admin/draws/delete", middleware.AdminOnly(db, admin.DeleteDrawHandler(db))) diff --git a/models/user.go b/models/user.go index 842c2d2..447337f 100644 --- a/models/user.go +++ b/models/user.go @@ -25,6 +25,7 @@ type Message struct { ID int Sender string Subject string + Message string IsRead bool CreatedAt time.Time } diff --git a/storage/badgecounts.go b/storage/badgecounts.go deleted file mode 100644 index 8ae8604..0000000 --- a/storage/badgecounts.go +++ /dev/null @@ -1,11 +0,0 @@ -package storage - -// "database/sql" - -// // Get all for count -// var count int -// db.Get(&count, `SELECT COUNT(*) FROM user_notifications WHERE user_id = ? AND is_read = FALSE`, userID) - -// // Then get the top 15 for display -// var notifications []Notification -// db.Select(¬ifications, `SELECT * FROM user_notifications WHERE user_id = ? AND is_read = FALSE ORDER BY created_at DESC LIMIT 15`, userID) diff --git a/storage/db.go b/storage/db.go index a7cda70..1fe87ec 100644 --- a/storage/db.go +++ b/storage/db.go @@ -13,222 +13,24 @@ func InitDB(filepath string) *sql.DB { log.Fatal("❌ Failed to open DB:", err) } - createThunderballResultsTable := ` - CREATE TABLE IF NOT EXISTS results_thunderball ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - draw_date TEXT NOT NULL UNIQUE, - machine TEXT, - ballset TEXT, - ball1 INTEGER, - ball2 INTEGER, - ball3 INTEGER, - ball4 INTEGER, - ball5 INTEGER, - thunderball INTEGER - );` - - if _, err := db.Exec(createThunderballResultsTable); err != nil { - log.Fatal("❌ Failed to create Thunderball table:", err) + schemas := []string{ + SchemaUsers, + SchemaThunderballResults, + SchemaThunderballPrizes, + SchemaLottoResults, + SchemaMyTickets, + SchemaUsersMessages, + SchemaUsersNotifications, + SchemaAuditLog, + SchemaLogTicketMatching, + SchemaAdminAccessLog, + SchemaNewAuditLog, } - createThunderballPrizeTable := ` - CREATE TABLE IF NOT EXISTS prizes_thunderball ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - draw_id INTEGER NOT NULL, - draw_date TEXT, - prize1 TEXT, - prize1_winners INTEGER, - prize1_per_winner INTEGER, - prize1_fund INTEGER, - prize2 TEXT, - prize2_winners INTEGER, - prize2_per_winner INTEGER, - prize2_fund INTEGER, - prize3 TEXT, - prize3_winners INTEGER, - prize3_per_winner INTEGER, - prize3_fund INTEGER, - prize4 TEXT, - prize4_winners INTEGER, - prize4_per_winner INTEGER, - prize4_fund INTEGER, - prize5 TEXT, - prize5_winners INTEGER, - prize5_per_winner INTEGER, - prize5_fund INTEGER, - prize6 TEXT, - prize6_winners INTEGER, - prize6_per_winner INTEGER, - prize6_fund INTEGER, - prize7 TEXT, - prize7_winners INTEGER, - prize7_per_winner INTEGER, - prize7_fund INTEGER, - prize8 TEXT, - prize8_winners INTEGER, - prize8_per_winner INTEGER, - prize8_fund INTEGER, - prize9 TEXT, - prize9_winners INTEGER, - prize9_per_winner INTEGER, - prize9_fund INTEGER, - total_winners INTEGER, - total_prize_fund INTEGER, - FOREIGN KEY (draw_date) REFERENCES results_thunderball(draw_date) - );` - - _, err = db.Exec(createThunderballPrizeTable) - if err != nil { - log.Fatal("❌ Failed to create Thunderball prize table:", err) - } - - createLottoResultsTable := ` - CREATE TABLE IF NOT EXISTS results_lotto ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - draw_date TEXT NOT NULL UNIQUE, - machine TEXT, - ballset TEXT, - ball1 INTEGER, - ball2 INTEGER, - ball3 INTEGER, - ball4 INTEGER, - ball5 INTEGER, - ball6 INTEGER, - bonusball INTEGER - );` - - if _, err := db.Exec(createLottoResultsTable); err != nil { - log.Fatal("❌ Failed to create Thunderball table:", err) - } - - createMyTicketsTable := ` - CREATE TABLE IF NOT EXISTS my_tickets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - userId INTEGER NOT NULL, - game_type TEXT NOT NULL, - draw_date TEXT NOT NULL, - ball1 INTEGER, - ball2 INTEGER, - ball3 INTEGER, - ball4 INTEGER, - ball5 INTEGER, - ball6 INTEGER, - bonus1 INTEGER, - bonus2 INTEGER, - duplicate BOOLEAN DEFAULT 0, - purchase_date TEXT, - purchase_method TEXT, - image_path TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - matched_main INTEGER, - matched_bonus INTEGER, - prize_tier TEXT, - is_winner BOOLEAN, - prize_amount INTEGER, - prize_label TEXT, - FOREIGN KEY (userId) REFERENCES users(id) - );` - - if _, err := db.Exec(createMyTicketsTable); err != nil { - log.Fatal("❌ Failed to create MyTickets table:", err) - } - - createUsersTable := ` - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT NOT NULL UNIQUE, - password_hash TEXT NOT NULL, - is_admin BOOLEAN - );` - - if _, err := db.Exec(createUsersTable); err != nil { - log.Fatal("❌ Failed to create Users table:", err) - } - - createUsersMessageTable := ` - CREATE TABLE IF NOT EXISTS users_messages ( - Id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL REFERENCES users(id), - title TEXT NOT NULL, - message TEXT, - is_read BOOLEAN DEFAULT FALSE, - type VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - );` - - if _, err := db.Exec(createUsersMessageTable); err != nil { - log.Fatal("❌ Failed to create Users messages table:", err) - } - - createUsersNotificationTable := ` - CREATE TABLE IF NOT EXISTS users_notification ( - Id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL REFERENCES users(id), - sender_name VARCHAR(100), - subject TEXT, - body TEXT, - is_read BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - );` - - if _, err := db.Exec(createUsersNotificationTable); err != nil { - log.Fatal("❌ Failed to create Users notification table:", err) - } - - createAuditLogTable := ` - CREATE TABLE IF NOT EXISTS auditlog ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT, - success INTEGER, - timestamp TEXT - );` - - if _, err := db.Exec(createAuditLogTable); err != nil { - log.Fatal("❌ Failed to create Users table:", err) - } - - createLogTicketMatchingTable := ` - CREATE TABLE IF NOT EXISTS log_ticket_matching ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - triggered_by TEXT, - run_at DATETIME DEFAULT CURRENT_TIMESTAMP, - tickets_matched INTEGER, - winners_found INTEGER, - notes TEXT - );` - - if _, err := db.Exec(createLogTicketMatchingTable); err != nil { - log.Fatal("❌ Failed to create ticket matching log table:", err) - } - - createAdminAccessLogTable := ` - CREATE TABLE IF NOT EXISTS admin_access_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER, - accessed_at DATETIME DEFAULT CURRENT_TIMESTAMP, - path TEXT, - ip TEXT, - user_agent TEXT - );` - - 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) + for _, stmt := range schemas { + if _, err := db.Exec(stmt); err != nil { + log.Fatalf("❌ Failed to apply schema: %v", err) + } } return db diff --git a/storage/messages.go b/storage/messages.go new file mode 100644 index 0000000..1854953 --- /dev/null +++ b/storage/messages.go @@ -0,0 +1,38 @@ +package storage + +import ( + "database/sql" + "synlotto-website/models" +) + +func GetMessageCount(db *sql.DB, userID int) (int, error) { + var count int + err := db.QueryRow(` + SELECT COUNT(*) FROM users_messages + WHERE user_id = ? AND is_read = FALSE + `, userID).Scan(&count) + return count, err +} + +func GetRecentMessages(db *sql.DB, userID int, limit int) []models.Message { + rows, err := db.Query(` + SELECT id, title, message, is_read + FROM users_messages + WHERE user_id = ? + ORDER BY created_at DESC + LIMIT ? + `, userID, limit) + if err != nil { + return nil + } + defer rows.Close() + + var messages []models.Message + for rows.Next() { + var m models.Message + rows.Scan(&m.ID, &m.Subject, &m.Message, &m.IsRead) + messages = append(messages, m) + } + + return messages +} diff --git a/storage/notifications.go b/storage/notifications.go new file mode 100644 index 0000000..6d300c7 --- /dev/null +++ b/storage/notifications.go @@ -0,0 +1,47 @@ +package storage + +import ( + "database/sql" + "log" + + "synlotto-website/models" +) + +func GetNotificationCount(db *sql.DB, userID int) int { + var count int + err := db.QueryRow(` + SELECT COUNT(*) FROM users_notification + WHERE user_id = ? AND is_read = FALSE`, userID).Scan(&count) + + if err != nil { + log.Println("⚠️ Failed to count notifications:", err) + return 0 + } + + return count +} + +func GetRecentNotifications(db *sql.DB, userID int, limit int) []models.Notification { + rows, err := db.Query(` + SELECT id, subject, body, is_read, created_at + FROM users_notification + WHERE user_id = ? + ORDER BY created_at DESC + LIMIT ?`, userID, limit) + if err != nil { + log.Println("⚠️ Failed to get notifications:", err) + return nil + } + defer rows.Close() + + var notifications []models.Notification + + for rows.Next() { + var n models.Notification + if err := rows.Scan(&n.ID, &n.Title, &n.Message, &n.IsRead, &n.CreatedAt); err == nil { + notifications = append(notifications, n) + } + } + + return notifications +} diff --git a/storage/schema.go b/storage/schema.go new file mode 100644 index 0000000..693b544 --- /dev/null +++ b/storage/schema.go @@ -0,0 +1,174 @@ +package storage + +const SchemaUsers = ` +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + is_admin BOOLEAN +);` + +const SchemaThunderballResults = ` +CREATE TABLE IF NOT EXISTS results_thunderball ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + draw_date TEXT NOT NULL UNIQUE, + machine TEXT, + ballset TEXT, + ball1 INTEGER, + ball2 INTEGER, + ball3 INTEGER, + ball4 INTEGER, + ball5 INTEGER, + thunderball INTEGER +);` + +const SchemaThunderballPrizes = ` +CREATE TABLE IF NOT EXISTS prizes_thunderball ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + draw_id INTEGER NOT NULL, + draw_date TEXT, + prize1 TEXT, + prize1_winners INTEGER, + prize1_per_winner INTEGER, + prize1_fund INTEGER, + prize2 TEXT, + prize2_winners INTEGER, + prize2_per_winner INTEGER, + prize2_fund INTEGER, + prize3 TEXT, + prize3_winners INTEGER, + prize3_per_winner INTEGER, + prize3_fund INTEGER, + prize4 TEXT, + prize4_winners INTEGER, + prize4_per_winner INTEGER, + prize4_fund INTEGER, + prize5 TEXT, + prize5_winners INTEGER, + prize5_per_winner INTEGER, + prize5_fund INTEGER, + prize6 TEXT, + prize6_winners INTEGER, + prize6_per_winner INTEGER, + prize6_fund INTEGER, + prize7 TEXT, + prize7_winners INTEGER, + prize7_per_winner INTEGER, + prize7_fund INTEGER, + prize8 TEXT, + prize8_winners INTEGER, + prize8_per_winner INTEGER, + prize8_fund INTEGER, + prize9 TEXT, + prize9_winners INTEGER, + prize9_per_winner INTEGER, + prize9_fund INTEGER, + total_winners INTEGER, + total_prize_fund INTEGER, + FOREIGN KEY (draw_date) REFERENCES results_thunderball(draw_date) +);` + +const SchemaLottoResults = ` +CREATE TABLE IF NOT EXISTS results_lotto ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + draw_date TEXT NOT NULL UNIQUE, + machine TEXT, + ballset TEXT, + ball1 INTEGER, + ball2 INTEGER, + ball3 INTEGER, + ball4 INTEGER, + ball5 INTEGER, + ball6 INTEGER, + bonusball INTEGER +);` + +const SchemaMyTickets = ` +CREATE TABLE IF NOT EXISTS my_tickets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + userId INTEGER NOT NULL, + game_type TEXT NOT NULL, + draw_date TEXT NOT NULL, + ball1 INTEGER, + ball2 INTEGER, + ball3 INTEGER, + ball4 INTEGER, + ball5 INTEGER, + ball6 INTEGER, + bonus1 INTEGER, + bonus2 INTEGER, + duplicate BOOLEAN DEFAULT 0, + purchase_date TEXT, + purchase_method TEXT, + image_path TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + matched_main INTEGER, + matched_bonus INTEGER, + prize_tier TEXT, + is_winner BOOLEAN, + prize_amount INTEGER, + prize_label TEXT, + FOREIGN KEY (userId) REFERENCES users(id) +);` + +const SchemaUsersMessages = ` +CREATE TABLE IF NOT EXISTS users_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id), + title TEXT NOT NULL, + message TEXT, + is_read BOOLEAN DEFAULT FALSE, + type VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +);` + +const SchemaUsersNotifications = ` +CREATE TABLE IF NOT EXISTS users_notification ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id), + sender_name VARCHAR(100), + subject TEXT, + body TEXT, + is_read BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +);` + +const SchemaAuditLog = ` +CREATE TABLE IF NOT EXISTS auditlog ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT, + success INTEGER, + timestamp TEXT +);` + +const SchemaLogTicketMatching = ` +CREATE TABLE IF NOT EXISTS log_ticket_matching ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + triggered_by TEXT, + run_at DATETIME DEFAULT CURRENT_TIMESTAMP, + tickets_matched INTEGER, + winners_found INTEGER, + notes TEXT +);` + +const SchemaAdminAccessLog = ` +CREATE TABLE IF NOT EXISTS admin_access_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + accessed_at DATETIME DEFAULT CURRENT_TIMESTAMP, + path TEXT, + ip TEXT, + user_agent TEXT +);` + +const SchemaNewAuditLog = ` +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 +);` diff --git a/templates/admin/draws/new_draw.html b/templates/admin/draws/new_draw.html index 19b653a..456d6c5 100644 --- a/templates/admin/draws/new_draw.html +++ b/templates/admin/draws/new_draw.html @@ -1,6 +1,6 @@ {{ define "new_draw" }}

Add New Draw

-
+ {{ .CSRFField }} diff --git a/templates/topbar.html b/templates/topbar.html index 8674605..2bf923c 100644 --- a/templates/topbar.html +++ b/templates/topbar.html @@ -1,44 +1,55 @@ {{ define "topbar" }} + + + + + + Hello, {{ .User.Username }} + Logout + {{ else }} + Login + {{ end }} + + {{ end }} \ No newline at end of file