more changes but unsure of state had to trash last set fof changes and try repair.
This commit is contained in:
2
go.mod
2
go.mod
@@ -6,8 +6,8 @@ require (
|
|||||||
github.com/gorilla/csrf v1.7.2
|
github.com/gorilla/csrf v1.7.2
|
||||||
github.com/gorilla/sessions v1.4.0
|
github.com/gorilla/sessions v1.4.0
|
||||||
golang.org/x/crypto v0.36.0
|
golang.org/x/crypto v0.36.0
|
||||||
modernc.org/sqlite v1.36.1
|
|
||||||
golang.org/x/time v0.11.0
|
golang.org/x/time v0.11.0
|
||||||
|
modernc.org/sqlite v1.36.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
64
handlers/home.go
Normal file
64
handlers/home.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
handlers/template_context.go
Normal file
60
handlers/template_context.go
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"synlotto-website/models"
|
"synlotto-website/models"
|
||||||
|
"synlotto-website/storage"
|
||||||
|
|
||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
)
|
)
|
||||||
@@ -50,6 +51,8 @@ func TemplateContext(w http.ResponseWriter, r *http.Request) map[string]interfac
|
|||||||
|
|
||||||
var currentUser *models.User
|
var currentUser *models.User
|
||||||
var isAdmin bool
|
var isAdmin bool
|
||||||
|
var notificationCount int
|
||||||
|
var notifications []models.Notification
|
||||||
|
|
||||||
switch v := session.Values["user_id"].(type) {
|
switch v := session.Values["user_id"].(type) {
|
||||||
case int:
|
case int:
|
||||||
@@ -60,6 +63,8 @@ func TemplateContext(w http.ResponseWriter, r *http.Request) map[string]interfac
|
|||||||
|
|
||||||
if currentUser != nil {
|
if currentUser != nil {
|
||||||
isAdmin = currentUser.IsAdmin
|
isAdmin = currentUser.IsAdmin
|
||||||
|
notificationCount = storage.GetNotificationCount(currentUser.Id)
|
||||||
|
notifications = storage.GetRecentNotifications(currentUser.Id, 15)
|
||||||
}
|
}
|
||||||
|
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
@@ -67,6 +72,8 @@ func TemplateContext(w http.ResponseWriter, r *http.Request) map[string]interfac
|
|||||||
"Flash": flash,
|
"Flash": flash,
|
||||||
"User": currentUser,
|
"User": currentUser,
|
||||||
"IsAdmin": isAdmin,
|
"IsAdmin": isAdmin,
|
||||||
|
"NotificationCount": notificationCount,
|
||||||
|
"Notifications": notifications,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,3 +107,9 @@ func rangeClass(n int) string {
|
|||||||
return "50-plus"
|
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)
|
||||||
|
}
|
||||||
|
|||||||
4
main.go
4
main.go
@@ -51,7 +51,9 @@ func setupAdminRoutes(mux *http.ServeMux, db *sql.DB) {
|
|||||||
mux.HandleFunc("/admin/triggers", middleware.AdminOnly(db, admin.AdminTriggersHandler(db)))
|
mux.HandleFunc("/admin/triggers", middleware.AdminOnly(db, admin.AdminTriggersHandler(db)))
|
||||||
|
|
||||||
// Draw management
|
// 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/modify", middleware.AdminOnly(db, admin.ModifyDrawHandler(db)))
|
||||||
mux.HandleFunc("/admin/draws/delete", middleware.AdminOnly(db, admin.DeleteDrawHandler(db)))
|
mux.HandleFunc("/admin/draws/delete", middleware.AdminOnly(db, admin.DeleteDrawHandler(db)))
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type Message struct {
|
|||||||
ID int
|
ID int
|
||||||
Sender string
|
Sender string
|
||||||
Subject string
|
Subject string
|
||||||
|
Message string
|
||||||
IsRead bool
|
IsRead bool
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
228
storage/db.go
228
storage/db.go
@@ -13,222 +13,24 @@ func InitDB(filepath string) *sql.DB {
|
|||||||
log.Fatal("❌ Failed to open DB:", err)
|
log.Fatal("❌ Failed to open DB:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
createThunderballResultsTable := `
|
schemas := []string{
|
||||||
CREATE TABLE IF NOT EXISTS results_thunderball (
|
SchemaUsers,
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
SchemaThunderballResults,
|
||||||
draw_date TEXT NOT NULL UNIQUE,
|
SchemaThunderballPrizes,
|
||||||
machine TEXT,
|
SchemaLottoResults,
|
||||||
ballset TEXT,
|
SchemaMyTickets,
|
||||||
ball1 INTEGER,
|
SchemaUsersMessages,
|
||||||
ball2 INTEGER,
|
SchemaUsersNotifications,
|
||||||
ball3 INTEGER,
|
SchemaAuditLog,
|
||||||
ball4 INTEGER,
|
SchemaLogTicketMatching,
|
||||||
ball5 INTEGER,
|
SchemaAdminAccessLog,
|
||||||
thunderball INTEGER
|
SchemaNewAuditLog,
|
||||||
);`
|
|
||||||
|
|
||||||
if _, err := db.Exec(createThunderballResultsTable); err != nil {
|
|
||||||
log.Fatal("❌ Failed to create Thunderball table:", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createThunderballPrizeTable := `
|
for _, stmt := range schemas {
|
||||||
CREATE TABLE IF NOT EXISTS prizes_thunderball (
|
if _, err := db.Exec(stmt); err != nil {
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
log.Fatalf("❌ Failed to apply schema: %v", err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return db
|
return db
|
||||||
|
|||||||
38
storage/messages.go
Normal file
38
storage/messages.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
47
storage/notifications.go
Normal file
47
storage/notifications.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
174
storage/schema.go
Normal file
174
storage/schema.go
Normal file
@@ -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
|
||||||
|
);`
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{{ define "new_draw" }}
|
{{ define "new_draw" }}
|
||||||
<h2 class="text-xl font-semibold mb-4">Add New Draw</h2>
|
<h2 class="text-xl font-semibold mb-4">Add New Draw</h2>
|
||||||
<form method="POST" action="/admin/draws/new">
|
<form method="POST" action="/admin/draws/submit">
|
||||||
{{ .CSRFField }}
|
{{ .CSRFField }}
|
||||||
<label class="block">Game Type: <input name="game_type" class="input"></label>
|
<label class="block">Game Type: <input name="game_type" class="input"></label>
|
||||||
<label class="block">Draw Date: <input name="draw_date" type="date" class="input"></label>
|
<label class="block">Draw Date: <input name="draw_date" type="date" class="input"></label>
|
||||||
|
|||||||
@@ -8,34 +8,45 @@
|
|||||||
{{ if .User }}
|
{{ if .User }}
|
||||||
{{ if .IsAdmin }}
|
{{ if .IsAdmin }}
|
||||||
<!-- Admin Dropdown -->
|
<!-- Admin Dropdown -->
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<a class="nav-link text-dark" href="#" id="adminDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<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>
|
<i class="bi bi-shield-lock fs-5 position-relative"></i>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end dropdown-admin-box shadow-sm dropdown-with-arrow" aria-labelledby="adminDropdown">
|
<ul class="dropdown-menu dropdown-menu-end dropdown-admin-box shadow-sm dropdown-with-arrow"
|
||||||
|
aria-labelledby="adminDropdown">
|
||||||
<li class="dropdown-header text-center fw-bold">Admin Menu</li>
|
<li class="dropdown-header text-center fw-bold">Admin Menu</li>
|
||||||
<li><hr class="dropdown-divider"></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">Tools</a></li>
|
||||||
<li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Audit Logs</a></li>
|
<li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Audit Logs</a></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</li>
|
||||||
<li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Open Dashboard</a></li>
|
<li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Open Dashboard</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<!-- Notification Dropdown
|
<!-- Notification Dropdown -->
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<a class="nav-link text-dark" href="#" id="notificationDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<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">
|
<i class="bi bi-bell fs-5 position-relative">
|
||||||
{{ if gt .NotificationCount 0 }}
|
{{ if gt (intVal .NotificationCount) 0 }}
|
||||||
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-warning text-dark badge-small">
|
<span
|
||||||
{{ if gt .NotificationCount 15 }}15+{{ else }}{{ .NotificationCount }}{{ end }}
|
class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-warning text-dark badge-small">
|
||||||
|
{{ if gt (intVal .NotificationCount) 15 }}15+{{ else }}{{ .NotificationCount }}{{ end }}
|
||||||
</span>
|
</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</i>
|
</i>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end dropdown-notification-box shadow-sm dropdown-with-arrow" aria-labelledby="notificationDropdown">
|
<ul class="dropdown-menu dropdown-menu-end dropdown-notification-box shadow-sm dropdown-with-arrow"
|
||||||
|
aria-labelledby="notificationDropdown">
|
||||||
<li class="dropdown-header text-center fw-bold">Notifications</li>
|
<li class="dropdown-header text-center fw-bold">Notifications</li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</li>
|
||||||
|
|
||||||
{{ $total := len .Notifications }}
|
{{ $total := len .Notifications }}
|
||||||
{{ range $i, $n := .Notifications }}
|
{{ range $i, $n := .Notifications }}
|
||||||
@@ -49,28 +60,37 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{{ if lt (add $i 1) $total }}
|
{{ if lt (add $i 1) $total }}
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<li class="text-center text-muted py-2">No notifications</li>
|
<li class="text-center text-muted py-2">No notifications</li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</li>
|
||||||
<li class="text-center"><a href="/notifications" class="dropdown-item">View all notifications</a></li>
|
<li class="text-center"><a href="/notifications" class="dropdown-item">View all notifications</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div> -->
|
</div>
|
||||||
|
|
||||||
<!-- Message Dropdown -->
|
<!-- Message Dropdown -->
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<a class="nav-link text-dark" href="#" id="messageDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<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">
|
<i class="bi bi-envelope fs-5 position-relative">
|
||||||
<!-- Unread badge (example: 2 messages) -->
|
<!-- Unread badge (example: 2 messages) -->
|
||||||
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-danger text-dark badge-small">2</span>
|
<span
|
||||||
|
class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-danger text-dark badge-small">2</span>
|
||||||
</i>
|
</i>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end dropdown-message-box shadow-sm dropdown-with-arrow" aria-labelledby="messageDropdown">
|
<ul class="dropdown-menu dropdown-menu-end dropdown-message-box shadow-sm dropdown-with-arrow"
|
||||||
|
aria-labelledby="messageDropdown">
|
||||||
<li class="dropdown-header text-center fw-bold">Messages</li>
|
<li class="dropdown-header text-center fw-bold">Messages</li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</li>
|
||||||
|
|
||||||
<!-- Example message item -->
|
<!-- Example message item -->
|
||||||
<li class="px-3 py-2">
|
<li class="px-3 py-2">
|
||||||
@@ -83,7 +103,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</li>
|
||||||
<li class="text-center"><a href="/messages" class="dropdown-item">View all messages</a></li>
|
<li class="text-center"><a href="/messages" class="dropdown-item">View all messages</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,6 +117,6 @@
|
|||||||
<a class="btn btn-outline-primary btn-sm" href="/login">Login</a>
|
<a class="btn btn-outline-primary btn-sm" href="/login">Login</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
Reference in New Issue
Block a user