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/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 (
|
||||
|
||||
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"
|
||||
"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)
|
||||
}
|
||||
|
||||
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)))
|
||||
|
||||
// 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)))
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ type Message struct {
|
||||
ID int
|
||||
Sender string
|
||||
Subject string
|
||||
Message string
|
||||
IsRead bool
|
||||
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)
|
||||
230
storage/db.go
230
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
|
||||
|
||||
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" }}
|
||||
<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 }}
|
||||
<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>
|
||||
|
||||
@@ -1,44 +1,55 @@
|
||||
{{ define "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 .IsAdmin }}
|
||||
<!-- Admin Dropdown -->
|
||||
<div class="dropdown">
|
||||
<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 dropdown-with-arrow" 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">
|
||||
{{ if gt .NotificationCount 0 }}
|
||||
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-warning text-dark badge-small">
|
||||
{{ if gt .NotificationCount 15 }}15+{{ else }}{{ .NotificationCount }}{{ end }}
|
||||
<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 .IsAdmin }}
|
||||
<!-- Admin Dropdown -->
|
||||
<div class="dropdown">
|
||||
<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 dropdown-with-arrow"
|
||||
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">
|
||||
{{ if gt (intVal .NotificationCount) 0 }}
|
||||
<span
|
||||
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>
|
||||
{{ end }}
|
||||
</i>
|
||||
</a>
|
||||
<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><hr class="dropdown-divider"></li>
|
||||
|
||||
{{ $total := len .Notifications }}
|
||||
{{ range $i, $n := .Notifications }}
|
||||
{{ end }}
|
||||
</i>
|
||||
</a>
|
||||
<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>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
|
||||
{{ $total := len .Notifications }}
|
||||
{{ range $i, $n := .Notifications }}
|
||||
<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>
|
||||
@@ -49,52 +60,63 @@
|
||||
</div>
|
||||
</li>
|
||||
{{ if lt (add $i 1) $total }}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
{{ else }}
|
||||
<li class="text-center text-muted py-2">No notifications</li>
|
||||
{{ end }}
|
||||
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li class="text-center"><a href="/notifications" class="dropdown-item">View all notifications</a></li>
|
||||
</ul>
|
||||
</div> -->
|
||||
{{ end }}
|
||||
|
||||
<!-- 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 badge-small">2</span>
|
||||
</i>
|
||||
</a>
|
||||
<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><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 }}
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li class="text-center"><a href="/notifications" class="dropdown-item">View all notifications</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 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 badge-small">2</span>
|
||||
</i>
|
||||
</a>
|
||||
<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>
|
||||
<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>
|
||||
</nav>
|
||||
|
||||
{{ end }}
|
||||
Reference in New Issue
Block a user