Refactor and remove sqlite and replace with MySQL
This commit is contained in:
141
internal/handlers/account/authentication.go
Normal file
141
internal/handlers/account/authentication.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
httpHelpers "synlotto-website/helpers/http"
|
||||
securityHelpers "synlotto-website/helpers/security"
|
||||
templateHelpers "synlotto-website/helpers/template"
|
||||
|
||||
"synlotto-website/logging"
|
||||
"synlotto-website/models"
|
||||
"synlotto-website/storage"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
)
|
||||
|
||||
func Login(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
session, _ := httpHelpers.GetSession(w, r)
|
||||
if _, ok := session.Values["user_id"].(int); ok {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("login.html", "templates/account/login.html")
|
||||
data := models.TemplateData{}
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
context["csrfField"] = csrf.TemplateField(r)
|
||||
|
||||
if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil {
|
||||
logging.Info("❌ Template render error:", err)
|
||||
http.Error(w, "Error rendering login page", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
|
||||
// ToDo: this outputs password in clear text remove or obscure!
|
||||
logging.Info("🔐 Login attempt - Username: %s, Password: %s", username, password)
|
||||
|
||||
user := storage.GetUserByUsername(db, username)
|
||||
if user == nil {
|
||||
logging.Info("❌ User not found: %s", username)
|
||||
storage.LogLoginAttempt(r, username, false)
|
||||
|
||||
session, _ := httpHelpers.GetSession(w, r)
|
||||
session.Values["flash"] = "Invalid username or password."
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if !securityHelpers.CheckPasswordHash(user.PasswordHash, password) {
|
||||
logging.Info("❌ Password mismatch for user: %s", username)
|
||||
storage.LogLoginAttempt(r, username, false)
|
||||
|
||||
session, _ := httpHelpers.GetSession(w, r)
|
||||
session.Values["flash"] = "Invalid username or password."
|
||||
session.Save(r, w)
|
||||
log.Printf("login has did it")
|
||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
logging.Info("✅ Login successful for user: %s", username)
|
||||
storage.LogLoginAttempt(r, username, true)
|
||||
|
||||
session, _ := httpHelpers.GetSession(w, r)
|
||||
for k := range session.Values {
|
||||
delete(session.Values, k)
|
||||
}
|
||||
|
||||
session.Values["user_id"] = user.Id
|
||||
session.Values["last_activity"] = time.Now().UTC()
|
||||
|
||||
if r.FormValue("remember") == "on" {
|
||||
session.Options.MaxAge = 60 * 60 * 24 * 30
|
||||
} else {
|
||||
session.Options.MaxAge = 0
|
||||
}
|
||||
|
||||
if err := session.Save(r, w); err != nil {
|
||||
logging.Info("❌ Failed to save session: %v", err)
|
||||
} else {
|
||||
logging.Info("✅ Session saved for user: %s", username)
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func Logout(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := httpHelpers.GetSession(w, r)
|
||||
|
||||
for k := range session.Values {
|
||||
delete(session.Values, k)
|
||||
}
|
||||
|
||||
session.Values["flash"] = "You've been logged out."
|
||||
session.Options.MaxAge = 5
|
||||
|
||||
err := session.Save(r, w)
|
||||
if err != nil {
|
||||
logging.Error("❌ Logout session save failed:", err)
|
||||
}
|
||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func Signup(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
tmpl := templateHelpers.LoadTemplateFiles("signup.html", "templates/account/signup.html")
|
||||
|
||||
tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{
|
||||
"csrfField": csrf.TemplateField(r),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
|
||||
hashed, err := securityHelpers.HashPassword(password)
|
||||
if err != nil {
|
||||
http.Error(w, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = models.CreateUser(username, hashed)
|
||||
if err != nil {
|
||||
http.Error(w, "Could not create user", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
||||
}
|
||||
96
internal/handlers/admin/audit.go
Normal file
96
internal/handlers/admin/audit.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
templateHelpers "synlotto-website/helpers/template"
|
||||
|
||||
"synlotto-website/middleware"
|
||||
"synlotto-website/models"
|
||||
)
|
||||
|
||||
type AdminLogEntry struct {
|
||||
AccessedAt string
|
||||
UserID int
|
||||
Path string
|
||||
IP string
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
func AdminAccessLogHandler(db *sql.DB) http.HandlerFunc {
|
||||
return middleware.Auth(true)(func(w http.ResponseWriter, r *http.Request) {
|
||||
data := models.TemplateData{}
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT accessed_at, user_id, path, ip, user_agent
|
||||
FROM admin_access_log
|
||||
ORDER BY accessed_at DESC
|
||||
LIMIT 100
|
||||
`)
|
||||
if err != nil {
|
||||
log.Println("⚠️ Failed to load admin access logs:", err)
|
||||
http.Error(w, "Error loading logs", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var logs []AdminLogEntry // ToDo should be in models
|
||||
for rows.Next() {
|
||||
var entry AdminLogEntry
|
||||
if err := rows.Scan(&entry.AccessedAt, &entry.UserID, &entry.Path, &entry.IP, &entry.UserAgent); err != nil {
|
||||
log.Println("⚠️ Scan failed:", err)
|
||||
continue
|
||||
}
|
||||
logs = append(logs, entry)
|
||||
}
|
||||
context["AuditLogs"] = logs
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("access_log.html", "templates/admin/logs/access_log.html")
|
||||
|
||||
_ = tmpl.ExecuteTemplate(w, "layout", context)
|
||||
})
|
||||
}
|
||||
|
||||
func AuditLogHandler(db *sql.DB) http.HandlerFunc {
|
||||
return middleware.Auth(true)(func(w http.ResponseWriter, r *http.Request) {
|
||||
data := models.TemplateData{}
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT timestamp, user_id, action, ip, user_agent
|
||||
FROM audit_log
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 100
|
||||
`)
|
||||
if err != nil {
|
||||
log.Println("❌ Failed to load audit log:", err)
|
||||
http.Error(w, "Could not load audit log", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var logs []models.AuditEntry
|
||||
for rows.Next() {
|
||||
var entry models.AuditEntry
|
||||
err := rows.Scan(&entry.Timestamp, &entry.UserID, &entry.Action, &entry.IP, &entry.UserAgent)
|
||||
if err != nil {
|
||||
log.Println("⚠️ Failed to scan row:", err)
|
||||
continue
|
||||
}
|
||||
logs = append(logs, entry)
|
||||
}
|
||||
|
||||
context["AuditLogs"] = logs
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("audit.html", "templates/admin/logs/audit.html")
|
||||
|
||||
err = tmpl.ExecuteTemplate(w, "layout", context)
|
||||
if err != nil {
|
||||
log.Println("❌ Failed to render audit page:", err)
|
||||
http.Error(w, "Template error", http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
76
internal/handlers/admin/dashboard.go
Normal file
76
internal/handlers/admin/dashboard.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
httpHelpers "synlotto-website/helpers/http"
|
||||
securityHelpers "synlotto-website/helpers/security"
|
||||
templateHelpers "synlotto-website/helpers/template"
|
||||
|
||||
"synlotto-website/models"
|
||||
"synlotto-website/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
total, winners int
|
||||
prizeSum float64
|
||||
)
|
||||
|
||||
func AdminDashboardHandler(db *sql.DB) http.HandlerFunc {
|
||||
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
user := storage.GetUserByID(db, userID)
|
||||
if user == nil {
|
||||
http.Error(w, "User not found", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
data := models.TemplateData{}
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
context["User"] = user
|
||||
context["IsAdmin"] = user.IsAdmin
|
||||
// Missing messages, notifications, potentially syndicate notifictions if that becomes a new top bar icon.
|
||||
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,
|
||||
}
|
||||
|
||||
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 := templateHelpers.LoadTemplateFiles("dashboard.html", "templates/admin/dashboard.html")
|
||||
|
||||
err = tmpl.ExecuteTemplate(w, "layout", context)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to render dashboard", http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
111
internal/handlers/admin/draws.go
Normal file
111
internal/handlers/admin/draws.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
httpHelpers "synlotto-website/helpers/http"
|
||||
templateHelpers "synlotto-website/helpers/template"
|
||||
|
||||
"synlotto-website/models"
|
||||
)
|
||||
|
||||
func NewDrawHandler(db *sql.DB) http.HandlerFunc {
|
||||
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
data := models.TemplateData{}
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
|
||||
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 := templateHelpers.LoadTemplateFiles("new_draw", "templates/admin/draws/new_draw.html")
|
||||
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
})
|
||||
}
|
||||
|
||||
func ModifyDrawHandler(db *sql.DB) http.HandlerFunc {
|
||||
return httpHelpers.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 httpHelpers.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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func ListDrawsHandler(db *sql.DB) http.HandlerFunc {
|
||||
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
data := models.TemplateData{}
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
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 := templateHelpers.LoadTemplateFiles("list.html", "templates/admin/draws/list.html")
|
||||
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
})
|
||||
}
|
||||
130
internal/handlers/admin/manualtriggers.go
Normal file
130
internal/handlers/admin/manualtriggers.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
templateHelpers "synlotto-website/helpers/template"
|
||||
services "synlotto-website/services/tickets"
|
||||
|
||||
"synlotto-website/models"
|
||||
)
|
||||
|
||||
func AdminTriggersHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data := models.TemplateData{}
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
|
||||
if flash := r.URL.Query().Get("flash"); flash != "" {
|
||||
context["Flash"] = flash
|
||||
}
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
action := r.FormValue("action")
|
||||
flashMsg := ""
|
||||
|
||||
switch action {
|
||||
case "match":
|
||||
stats, err := services.RunTicketMatching(db, "manual")
|
||||
if err != nil {
|
||||
http.Error(w, "Matching failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
flashMsg = fmt.Sprintf("✅ Matched %d tickets, %d winners.", stats.TicketsMatched, stats.WinnersFound)
|
||||
|
||||
case "prizes":
|
||||
err := services.UpdateMissingPrizes(db)
|
||||
if err != nil {
|
||||
http.Error(w, "Prize update failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
flashMsg = "✅ Missing prizes updated."
|
||||
|
||||
case "refresh_prizes":
|
||||
err := services.RefreshTicketPrizes(db)
|
||||
if err != nil {
|
||||
http.Error(w, "Refresh failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
flashMsg = "✅ Ticket prizes refreshed."
|
||||
|
||||
case "run_all":
|
||||
stats, err := services.RunTicketMatching(db, "manual")
|
||||
if err != nil {
|
||||
http.Error(w, "Matching failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = services.UpdateMissingPrizes(db)
|
||||
if err != nil {
|
||||
http.Error(w, "Prize update failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
flashMsg = fmt.Sprintf("✅ Matched %d tickets, %d winners. Prizes updated.", stats.TicketsMatched, stats.WinnersFound)
|
||||
|
||||
default:
|
||||
flashMsg = "⚠️ Unknown action."
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/admin/triggers?flash="+url.QueryEscape(flashMsg), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("triggers.html", "templates/admin/triggers.html")
|
||||
|
||||
err := tmpl.ExecuteTemplate(w, "layout", context)
|
||||
if err != nil {
|
||||
log.Println("Template error:", err)
|
||||
http.Error(w, "Failed to load page", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func MatchTicketsHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
stats, err := services.RunTicketMatching(db, "manual")
|
||||
if err != nil {
|
||||
http.Error(w, "Matching failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/admin/triggers?flash=Matched "+
|
||||
strconv.Itoa(stats.TicketsMatched)+" tickets, "+
|
||||
strconv.Itoa(stats.WinnersFound)+" winners.", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateMissingPrizesHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
err := services.UpdateMissingPrizes(db)
|
||||
if err != nil {
|
||||
http.Error(w, "Prize update failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/admin/triggers?flash=Updated missing prize data.", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func RunAllHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
stats, err := services.RunTicketMatching(db, "manual")
|
||||
if err != nil {
|
||||
http.Error(w, "Matching failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = services.UpdateMissingPrizes(db)
|
||||
if err != nil {
|
||||
http.Error(w, "Prize update failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/admin/triggers?flash=Matched "+
|
||||
strconv.Itoa(stats.TicketsMatched)+" tickets, "+
|
||||
strconv.Itoa(stats.WinnersFound)+" winners. Prizes updated.", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
70
internal/handlers/admin/prizes.go
Normal file
70
internal/handlers/admin/prizes.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httpHelpers "synlotto-website/helpers/http"
|
||||
templateHelpers "synlotto-website/helpers/template"
|
||||
|
||||
"synlotto-website/models"
|
||||
)
|
||||
|
||||
func AddPrizesHandler(db *sql.DB) http.HandlerFunc {
|
||||
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
data := models.TemplateData{}
|
||||
if r.Method == http.MethodGet {
|
||||
tmpl := templateHelpers.LoadTemplateFiles("add_prizes.html", "templates/admin/draws/prizes/add_prizes.html")
|
||||
tmpl.ExecuteTemplate(w, "layout", templateHelpers.TemplateContext(w, r, data))
|
||||
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 httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
data := models.TemplateData{}
|
||||
if r.Method == http.MethodGet {
|
||||
tmpl := templateHelpers.LoadTemplateFiles("modify_prizes.html", "templates/admin/draws/prizes/modify_prizes.html")
|
||||
|
||||
tmpl.ExecuteTemplate(w, "layout", templateHelpers.TemplateContext(w, r, data))
|
||||
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)
|
||||
})
|
||||
}
|
||||
8
internal/handlers/common.go
Normal file
8
internal/handlers/common.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"synlotto-website/models"
|
||||
)
|
||||
|
||||
var Draws []models.ThunderballResult
|
||||
var MyTickets []models.Ticket
|
||||
25
internal/handlers/home.go
Normal file
25
internal/handlers/home.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
templateHandlers "synlotto-website/handlers/template"
|
||||
templateHelpers "synlotto-website/helpers/template"
|
||||
)
|
||||
|
||||
func Home(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("index.html", "templates/index.html")
|
||||
|
||||
err := tmpl.ExecuteTemplate(w, "layout", context)
|
||||
if err != nil {
|
||||
log.Println("❌ Template render error:", err)
|
||||
http.Error(w, "Error rendering homepage", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
60
internal/handlers/lottery/draws/draw_handler.go
Normal file
60
internal/handlers/lottery/draws/draw_handler.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
templateHelpers "synlotto-website/helpers/template"
|
||||
|
||||
"synlotto-website/helpers"
|
||||
"synlotto-website/models"
|
||||
"synlotto-website/storage"
|
||||
)
|
||||
|
||||
func NewDraw(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data := models.TemplateData{}
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
context["Page"] = "new_draw"
|
||||
context["Data"] = nil
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("new_draw.html", "templates/admin/draws/new_draw.html") // ToDo: may need removing or moving add draw should be admin functionality and only when manually required. Potential live drawing of numbers in the future.
|
||||
|
||||
err := tmpl.ExecuteTemplate(w, "layout", context)
|
||||
if err != nil {
|
||||
log.Println("❌ Template error:", err)
|
||||
http.Error(w, "Error rendering form", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Submit(db *sql.DB, w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("📝 Form submission received")
|
||||
_ = r.ParseForm()
|
||||
|
||||
draw := models.ThunderballResult{
|
||||
DrawDate: r.FormValue("date"),
|
||||
Machine: r.FormValue("machine"),
|
||||
BallSet: helpers.Atoi(r.FormValue("ballSet")),
|
||||
Ball1: helpers.Atoi(r.FormValue("ball1")),
|
||||
Ball2: helpers.Atoi(r.FormValue("ball2")),
|
||||
Ball3: helpers.Atoi(r.FormValue("ball3")),
|
||||
Ball4: helpers.Atoi(r.FormValue("ball4")),
|
||||
Ball5: helpers.Atoi(r.FormValue("ball5")),
|
||||
Thunderball: helpers.Atoi(r.FormValue("thunderball")),
|
||||
}
|
||||
|
||||
err := storage.InsertThunderballResult(db, draw)
|
||||
if err != nil {
|
||||
log.Println("❌ Failed to insert draw:", err)
|
||||
http.Error(w, "Failed to save draw", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("📅 %s | 🛠 %s | 🎱 %d | 🔢 %d,%d,%d,%d,%d | ⚡ %d\n",
|
||||
draw.DrawDate, draw.Machine, draw.BallSet,
|
||||
draw.Ball1, draw.Ball2, draw.Ball3, draw.Ball4, draw.Ball5, draw.Thunderball)
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
203
internal/handlers/lottery/syndicate/syndicate.go
Normal file
203
internal/handlers/lottery/syndicate/syndicate.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
templateHandlers "synlotto-website/handlers/template"
|
||||
securityHelpers "synlotto-website/helpers/security"
|
||||
templateHelpers "synlotto-website/helpers/template"
|
||||
|
||||
"synlotto-website/helpers"
|
||||
"synlotto-website/models"
|
||||
"synlotto-website/storage"
|
||||
)
|
||||
|
||||
func CreateSyndicateHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
tmpl := templateHelpers.LoadTemplateFiles("create-syndicate.html", "templates/syndicate/create.html")
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
|
||||
case http.MethodPost:
|
||||
name := r.FormValue("name")
|
||||
description := r.FormValue("description")
|
||||
|
||||
userId, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok || name == "" {
|
||||
templateHelpers.SetFlash(w, r, "Invalid data submitted")
|
||||
http.Redirect(w, r, "/syndicate/create", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := storage.CreateSyndicate(db, userId, name, description)
|
||||
if err != nil {
|
||||
log.Printf("❌ CreateSyndicate failed: %v", err)
|
||||
templateHelpers.SetFlash(w, r, "Failed to create syndicate")
|
||||
} else {
|
||||
templateHelpers.SetFlash(w, r, "Syndicate created successfully")
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
|
||||
default:
|
||||
templateHelpers.RenderError(w, r, http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ListSyndicatesHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
templateHelpers.RenderError(w, r, 403) // ToDo need to make this use the handler so i dont need to define errors.
|
||||
return
|
||||
}
|
||||
|
||||
managed := storage.GetSyndicatesByOwner(db, userID)
|
||||
member := storage.GetSyndicatesByMember(db, userID)
|
||||
|
||||
managedMap := make(map[int]bool)
|
||||
for _, s := range managed {
|
||||
managedMap[s.ID] = true
|
||||
}
|
||||
|
||||
var filteredJoined []models.Syndicate
|
||||
for _, s := range member {
|
||||
if !managedMap[s.ID] {
|
||||
filteredJoined = append(filteredJoined, s)
|
||||
}
|
||||
}
|
||||
|
||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
context["ManagedSyndicates"] = managed
|
||||
context["JoinedSyndicates"] = filteredJoined
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("syndicates.html", "templates/syndicate/index.html")
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
}
|
||||
}
|
||||
|
||||
func ViewSyndicateHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
templateHelpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||
syndicate, err := storage.GetSyndicateByID(db, syndicateID)
|
||||
if err != nil || syndicate == nil {
|
||||
templateHelpers.RenderError(w, r, 404)
|
||||
return
|
||||
}
|
||||
|
||||
isManager := userID == syndicate.OwnerID
|
||||
isMember := storage.IsSyndicateMember(db, syndicateID, userID)
|
||||
|
||||
if !isManager && !isMember {
|
||||
templateHelpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
members := storage.GetSyndicateMembers(db, syndicateID)
|
||||
|
||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
context["Syndicate"] = syndicate
|
||||
context["Members"] = members
|
||||
context["IsManager"] = isManager
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("syndicate-view.html", "templates/syndicate/view.html")
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
}
|
||||
}
|
||||
|
||||
func SyndicateLogTicketHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
templateHelpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
syndicateId := helpers.Atoi(r.URL.Query().Get("id"))
|
||||
syndicate, err := storage.GetSyndicateByID(db, syndicateId)
|
||||
if err != nil || syndicate.OwnerID != userID {
|
||||
templateHelpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
context["Syndicate"] = syndicate
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("syndicate-log-ticket.html", "templates/syndicate/log_ticket.html")
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
|
||||
case http.MethodPost:
|
||||
gameType := r.FormValue("game_type")
|
||||
drawDate := r.FormValue("draw_date")
|
||||
method := r.FormValue("purchase_method")
|
||||
|
||||
err := storage.InsertTicket(db, models.Ticket{
|
||||
UserId: userID,
|
||||
GameType: gameType,
|
||||
DrawDate: drawDate,
|
||||
PurchaseMethod: method,
|
||||
SyndicateId: &syndicateId,
|
||||
// ToDo image path
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
templateHelpers.SetFlash(w, r, "Failed to add ticket.")
|
||||
} else {
|
||||
templateHelpers.SetFlash(w, r, "Ticket added for syndicate.")
|
||||
}
|
||||
|
||||
http.Redirect(w, r, fmt.Sprintf("/syndicate/view?id=%d", syndicateId), http.StatusSeeOther)
|
||||
|
||||
default:
|
||||
templateHelpers.RenderError(w, r, 405)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SyndicateTicketsHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
templateHelpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||
if syndicateID == 0 {
|
||||
templateHelpers.RenderError(w, r, 400)
|
||||
return
|
||||
}
|
||||
|
||||
if !storage.IsSyndicateMember(db, syndicateID, userID) {
|
||||
templateHelpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
tickets := storage.GetSyndicateTickets(db, syndicateID)
|
||||
|
||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
context["SyndicateID"] = syndicateID
|
||||
context["Tickets"] = tickets
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("syndicate-tickets.html", "templates/syndicate/tickets.html")
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
}
|
||||
}
|
||||
226
internal/handlers/lottery/syndicate/syndicate_invites.go
Normal file
226
internal/handlers/lottery/syndicate/syndicate_invites.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
templateHandlers "synlotto-website/handlers/template"
|
||||
securityHelpers "synlotto-website/helpers/security"
|
||||
templateHelpers "synlotto-website/helpers/template"
|
||||
|
||||
"synlotto-website/helpers"
|
||||
"synlotto-website/storage"
|
||||
)
|
||||
|
||||
func SyndicateInviteHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
context["SyndicateID"] = syndicateID
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("invite-syndicate.html", "templates/syndicate/invite.html")
|
||||
err := tmpl.ExecuteTemplate(w, "layout", context)
|
||||
if err != nil {
|
||||
templateHelpers.RenderError(w, r, 500)
|
||||
}
|
||||
case http.MethodPost:
|
||||
syndicateID := helpers.Atoi(r.FormValue("syndicate_id"))
|
||||
username := r.FormValue("username")
|
||||
|
||||
err := storage.InviteToSyndicate(db, userID, syndicateID, username)
|
||||
if err != nil {
|
||||
templateHelpers.SetFlash(w, r, "Failed to send invite: "+err.Error())
|
||||
} else {
|
||||
templateHelpers.SetFlash(w, r, "Invite sent successfully.")
|
||||
}
|
||||
http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther)
|
||||
|
||||
default:
|
||||
templateHelpers.RenderError(w, r, http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ViewInvitesHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
templateHelpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
invites := storage.GetPendingInvites(db, userID)
|
||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
context["Invites"] = invites
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("invites.html", "templates/syndicate/invites.html")
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
}
|
||||
}
|
||||
|
||||
func AcceptInviteHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
inviteID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
templateHelpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
err := storage.AcceptInvite(db, inviteID, userID)
|
||||
if err != nil {
|
||||
templateHelpers.SetFlash(w, r, "Failed to accept invite")
|
||||
} else {
|
||||
templateHelpers.SetFlash(w, r, "You have joined the syndicate")
|
||||
}
|
||||
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func DeclineInviteHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
inviteID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||
_ = storage.UpdateInviteStatus(db, inviteID, "declined")
|
||||
http.Redirect(w, r, "/syndicate/invites", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateInviteToken(db *sql.DB, syndicateID, invitedByID int, ttlHours int) (string, error) {
|
||||
token, err := securityHelpers.GenerateSecureToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
expires := time.Now().Add(time.Duration(ttlHours) * time.Hour)
|
||||
|
||||
_, err = db.Exec(`
|
||||
INSERT INTO syndicate_invite_tokens (syndicate_id, token, invited_by_user_id, expires_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, syndicateID, token, invitedByID, expires)
|
||||
|
||||
return token, err
|
||||
}
|
||||
|
||||
func AcceptInviteToken(db *sql.DB, token string, userID int) error {
|
||||
var syndicateID int
|
||||
var expiresAt, acceptedAt sql.NullTime
|
||||
err := db.QueryRow(`
|
||||
SELECT syndicate_id, expires_at, accepted_at
|
||||
FROM syndicate_invite_tokens
|
||||
WHERE token = ?
|
||||
`, token).Scan(&syndicateID, &expiresAt, &acceptedAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid or expired token")
|
||||
}
|
||||
if acceptedAt.Valid || expiresAt.Time.Before(time.Now()) {
|
||||
return fmt.Errorf("token already used or expired")
|
||||
}
|
||||
|
||||
_, err = db.Exec(`
|
||||
INSERT INTO syndicate_members (syndicate_id, user_id, role, status, joined_at)
|
||||
VALUES (?, ?, 'member', 'active', CURRENT_TIMESTAMP)
|
||||
`, syndicateID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.Exec(`
|
||||
UPDATE syndicate_invite_tokens
|
||||
SET accepted_by_user_id = ?, accepted_at = CURRENT_TIMESTAMP
|
||||
WHERE token = ?
|
||||
`, userID, token)
|
||||
return err
|
||||
}
|
||||
|
||||
func GenerateInviteLinkHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||
token, err := CreateInviteToken(db, syndicateID, userID, 48)
|
||||
if err != nil {
|
||||
templateHelpers.SetFlash(w, r, "Failed to generate invite link.")
|
||||
http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
origin := r.Host
|
||||
if r.TLS != nil {
|
||||
origin = "https://" + origin
|
||||
} else {
|
||||
origin = "http://" + origin
|
||||
}
|
||||
inviteLink := fmt.Sprintf("%s/syndicate/join?token=%s", origin, token)
|
||||
|
||||
templateHelpers.SetFlash(w, r, "Invite link created: "+inviteLink)
|
||||
http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func JoinSyndicateWithTokenHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
token := r.URL.Query().Get("token")
|
||||
if token == "" {
|
||||
templateHelpers.SetFlash(w, r, "Invalid or missing invite token.")
|
||||
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
err := AcceptInviteToken(db, token, userID)
|
||||
if err != nil {
|
||||
templateHelpers.SetFlash(w, r, "Failed to join syndicate: "+err.Error())
|
||||
} else {
|
||||
templateHelpers.SetFlash(w, r, "You have joined the syndicate!")
|
||||
}
|
||||
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func ManageInviteTokensHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
templateHelpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||
|
||||
if !storage.IsSyndicateManager(db, syndicateID, userID) {
|
||||
templateHelpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
tokens := storage.GetInviteTokensForSyndicate(db, syndicateID)
|
||||
|
||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
context["Tokens"] = tokens
|
||||
context["SyndicateID"] = syndicateID
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("invite-links.html", "templates/syndicate/invite_links.html")
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
}
|
||||
}
|
||||
375
internal/handlers/lottery/tickets/ticket_handler.go
Normal file
375
internal/handlers/lottery/tickets/ticket_handler.go
Normal file
@@ -0,0 +1,375 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
httpHelpers "synlotto-website/helpers/http"
|
||||
securityHelpers "synlotto-website/helpers/security"
|
||||
templateHelpers "synlotto-website/helpers/template"
|
||||
draws "synlotto-website/services/draws"
|
||||
|
||||
"synlotto-website/helpers"
|
||||
"synlotto-website/models"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
)
|
||||
|
||||
func AddTicket(db *sql.DB) http.HandlerFunc {
|
||||
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
rows, err := db.Query(`
|
||||
SELECT DISTINCT draw_date
|
||||
FROM results_thunderball
|
||||
ORDER BY draw_date DESC
|
||||
`)
|
||||
if err != nil {
|
||||
log.Println("❌ Failed to load draw dates:", err)
|
||||
http.Error(w, "Unable to load draw dates", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var drawDates []string
|
||||
for rows.Next() {
|
||||
var date string
|
||||
if err := rows.Scan(&date); err == nil {
|
||||
drawDates = append(drawDates, date)
|
||||
}
|
||||
}
|
||||
|
||||
data := models.TemplateData{}
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
context["csrfField"] = csrf.TemplateField(r)
|
||||
context["DrawDates"] = drawDates
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("add_ticket.html", "templates/account/tickets/add_ticket.html")
|
||||
|
||||
err = tmpl.ExecuteTemplate(w, "layout", context)
|
||||
if err != nil {
|
||||
log.Println("❌ Template render error:", err)
|
||||
http.Error(w, "Error rendering form", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err := r.ParseMultipartForm(10 << 20)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid form", http.StatusBadRequest)
|
||||
log.Println("❌ Failed to parse form:", err)
|
||||
return
|
||||
}
|
||||
|
||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
game := r.FormValue("game_type")
|
||||
drawDate := r.FormValue("draw_date")
|
||||
purchaseMethod := r.FormValue("purchase_method")
|
||||
purchaseDate := r.FormValue("purchase_date")
|
||||
purchaseTime := r.FormValue("purchase_time")
|
||||
|
||||
if purchaseTime != "" {
|
||||
purchaseDate += "T" + purchaseTime
|
||||
}
|
||||
|
||||
imagePath := ""
|
||||
file, handler, err := r.FormFile("ticket_image")
|
||||
if err == nil && handler != nil {
|
||||
defer file.Close()
|
||||
filename := fmt.Sprintf("uploads/ticket_%d_%s", time.Now().UnixNano(), handler.Filename)
|
||||
out, err := os.Create(filename)
|
||||
if err == nil {
|
||||
defer out.Close()
|
||||
io.Copy(out, file)
|
||||
imagePath = filename
|
||||
}
|
||||
}
|
||||
|
||||
var ballCount, bonusCount int
|
||||
switch game {
|
||||
case "Thunderball":
|
||||
ballCount, bonusCount = 5, 1
|
||||
case "Lotto":
|
||||
ballCount, bonusCount = 6, 0
|
||||
case "EuroMillions":
|
||||
ballCount, bonusCount = 5, 2
|
||||
case "SetForLife":
|
||||
ballCount, bonusCount = 5, 1
|
||||
default:
|
||||
http.Error(w, "Unsupported game type", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
balls := make([][]int, ballCount)
|
||||
bonuses := make([][]int, bonusCount)
|
||||
|
||||
for i := 1; i <= ballCount; i++ {
|
||||
field := fmt.Sprintf("ball%d[]", i)
|
||||
balls[i-1] = helpers.ParseIntSlice(r.Form[field])
|
||||
log.Printf("🔢 %s: %v", field, balls[i-1])
|
||||
}
|
||||
for i := 1; i <= bonusCount; i++ {
|
||||
field := fmt.Sprintf("bonus%d[]", i)
|
||||
bonuses[i-1] = helpers.ParseIntSlice(r.Form[field])
|
||||
log.Printf("🎯 %s: %v", field, bonuses[i-1])
|
||||
}
|
||||
|
||||
lineCount := 0
|
||||
if len(balls) > 0 {
|
||||
lineCount = len(balls[0])
|
||||
}
|
||||
log.Println("🧾 Total lines to insert:", lineCount)
|
||||
|
||||
for i := 0; i < lineCount; i++ {
|
||||
b := make([]int, 6)
|
||||
bo := make([]int, 2)
|
||||
|
||||
valid := true
|
||||
for j := 0; j < ballCount; j++ {
|
||||
if j < len(balls) && i < len(balls[j]) {
|
||||
b[j] = balls[j][i]
|
||||
if b[j] == 0 {
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
}
|
||||
for j := 0; j < bonusCount; j++ {
|
||||
if j < len(bonuses) && i < len(bonuses[j]) {
|
||||
bo[j] = bonuses[j][i]
|
||||
if bo[j] == 0 {
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !valid {
|
||||
log.Printf("⚠️ Skipping invalid line %d (incomplete values)", i+1)
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO my_tickets (
|
||||
userId, game_type, draw_date,
|
||||
ball1, ball2, ball3, ball4, ball5, ball6,
|
||||
bonus1, bonus2,
|
||||
purchase_method, purchase_date, image_path
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
userID, game, drawDate,
|
||||
b[0], b[1], b[2], b[3], b[4], b[5],
|
||||
bo[0], bo[1],
|
||||
purchaseMethod, purchaseDate, imagePath,
|
||||
)
|
||||
if err != nil {
|
||||
log.Println("❌ Failed to insert ticket line:", err)
|
||||
} else {
|
||||
log.Printf("✅ Ticket line %d saved", i+1) // ToDo create audit
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/tickets", http.StatusSeeOther)
|
||||
})
|
||||
}
|
||||
|
||||
func SubmitTicket(db *sql.DB) http.HandlerFunc {
|
||||
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseMultipartForm(10 << 20)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
game := r.FormValue("game_type")
|
||||
drawDate := r.FormValue("draw_date")
|
||||
purchaseMethod := r.FormValue("purchase_method")
|
||||
purchaseDate := r.FormValue("purchase_date")
|
||||
purchaseTime := r.FormValue("purchase_time")
|
||||
|
||||
if purchaseTime != "" {
|
||||
purchaseDate += "T" + purchaseTime
|
||||
}
|
||||
|
||||
imagePath := ""
|
||||
file, handler, err := r.FormFile("ticket_image")
|
||||
if err == nil && handler != nil {
|
||||
defer file.Close()
|
||||
filename := fmt.Sprintf("uploads/ticket_%d_%s", time.Now().UnixNano(), handler.Filename)
|
||||
out, err := os.Create(filename)
|
||||
if err == nil {
|
||||
defer out.Close()
|
||||
io.Copy(out, file)
|
||||
imagePath = filename
|
||||
}
|
||||
}
|
||||
|
||||
ballCount := 6
|
||||
bonusCount := 2
|
||||
|
||||
balls := make([][]int, ballCount)
|
||||
bonuses := make([][]int, bonusCount)
|
||||
|
||||
for i := 1; i <= ballCount; i++ {
|
||||
balls[i-1] = helpers.ParseIntSlice(r.Form["ball"+strconv.Itoa(i)])
|
||||
}
|
||||
for i := 1; i <= bonusCount; i++ {
|
||||
bonuses[i-1] = helpers.ParseIntSlice(r.Form["bonus"+strconv.Itoa(i)])
|
||||
}
|
||||
|
||||
lineCount := len(balls[0])
|
||||
for i := 0; i < lineCount; i++ {
|
||||
var b [6]int
|
||||
var bo [2]int
|
||||
|
||||
for j := 0; j < ballCount; j++ {
|
||||
if j < len(balls) && i < len(balls[j]) {
|
||||
b[j] = balls[j][i]
|
||||
}
|
||||
}
|
||||
for j := 0; j < bonusCount; j++ {
|
||||
if j < len(bonuses) && i < len(bonuses[j]) {
|
||||
bo[j] = bonuses[j][i]
|
||||
}
|
||||
}
|
||||
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO my_tickets (
|
||||
user_id, game_type, draw_date,
|
||||
ball1, ball2, ball3, ball4, ball5, ball6,
|
||||
bonus1, bonus2,
|
||||
purchase_method, purchase_date, image_path
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
userID, game, drawDate,
|
||||
b[0], b[1], b[2], b[3], b[4], b[5],
|
||||
bo[0], bo[1],
|
||||
purchaseMethod, purchaseDate, imagePath,
|
||||
)
|
||||
if err != nil {
|
||||
log.Println("❌ Insert failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/tickets", http.StatusSeeOther)
|
||||
})
|
||||
}
|
||||
|
||||
func GetMyTickets(db *sql.DB) http.HandlerFunc {
|
||||
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
data := models.TemplateData{}
|
||||
var tickets []models.Ticket
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
context["Tickets"] = tickets
|
||||
|
||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT id, game_type, draw_date,
|
||||
ball1, ball2, ball3, ball4, ball5, ball6,
|
||||
bonus1, bonus2,
|
||||
purchase_method, purchase_date, image_path, duplicate,
|
||||
matched_main, matched_bonus, prize_tier, is_winner, prize_label, prize_amount
|
||||
FROM my_tickets
|
||||
WHERE userid = ?
|
||||
ORDER BY draw_date DESC, created_at DESC
|
||||
`, userID)
|
||||
if err != nil {
|
||||
log.Println("❌ Query failed:", err)
|
||||
http.Error(w, "Could not load tickets", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var t models.Ticket
|
||||
var b1, b2, b3, b4, b5, b6, bo1, bo2 sql.NullInt64
|
||||
var matchedMain, matchedBonus sql.NullInt64
|
||||
var prizeTier sql.NullString
|
||||
var isWinner sql.NullBool
|
||||
var prizeLabel sql.NullString
|
||||
var prizeAmount sql.NullFloat64
|
||||
|
||||
err := rows.Scan(
|
||||
&t.Id, &t.GameType, &t.DrawDate,
|
||||
&b1, &b2, &b3, &b4, &b5, &b6,
|
||||
&bo1, &bo2,
|
||||
&t.PurchaseMethod, &t.PurchaseDate, &t.ImagePath, &t.Duplicate,
|
||||
&matchedMain, &matchedBonus, &prizeTier, &isWinner, &prizeLabel, &prizeAmount,
|
||||
)
|
||||
if err != nil {
|
||||
log.Println("⚠️ Failed to scan ticket row:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Build primary number + bonus fields
|
||||
t.Ball1 = int(b1.Int64)
|
||||
t.Ball2 = int(b2.Int64)
|
||||
t.Ball3 = int(b3.Int64)
|
||||
t.Ball4 = int(b4.Int64)
|
||||
t.Ball5 = int(b5.Int64)
|
||||
t.Ball6 = int(b6.Int64)
|
||||
t.Bonus1 = helpers.IntPtrIfValid(bo1)
|
||||
t.Bonus2 = helpers.IntPtrIfValid(bo2)
|
||||
|
||||
if matchedMain.Valid {
|
||||
t.MatchedMain = int(matchedMain.Int64)
|
||||
}
|
||||
if matchedBonus.Valid {
|
||||
t.MatchedBonus = int(matchedBonus.Int64)
|
||||
}
|
||||
if prizeTier.Valid {
|
||||
t.PrizeTier = prizeTier.String
|
||||
}
|
||||
if isWinner.Valid {
|
||||
t.IsWinner = isWinner.Bool
|
||||
}
|
||||
if prizeLabel.Valid {
|
||||
t.PrizeLabel = prizeLabel.String
|
||||
}
|
||||
if prizeAmount.Valid {
|
||||
t.PrizeAmount = prizeAmount.Float64
|
||||
}
|
||||
// Build balls slices (for template use)
|
||||
t.Balls = helpers.BuildBallsSlice(t)
|
||||
t.BonusBalls = helpers.BuildBonusSlice(t)
|
||||
|
||||
// 🎯 Get the actual draw info (used to show which numbers matched)
|
||||
draw := draws.GetDrawResultForTicket(db, t.GameType, t.DrawDate)
|
||||
t.MatchedDraw = draw
|
||||
|
||||
// ✅ DEBUG
|
||||
log.Printf("✅ Ticket #%d", t.Id)
|
||||
log.Printf("Balls: %v", t.Balls)
|
||||
log.Printf("DrawResult: %+v", draw)
|
||||
|
||||
tickets = append(tickets, t)
|
||||
}
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("my_tickets.html", "templates/account/tickets/my_tickets.html")
|
||||
|
||||
err = tmpl.ExecuteTemplate(w, "layout", context)
|
||||
if err != nil {
|
||||
log.Println("❌ Template error:", err)
|
||||
http.Error(w, "Error rendering page", http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
44
internal/handlers/lottery/tickets/ticket_matcher.go
Normal file
44
internal/handlers/lottery/tickets/ticket_matcher.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"synlotto-website/models"
|
||||
)
|
||||
|
||||
func MatchTicketToDraw(ticket models.MatchTicket, draw models.DrawResult, rules []models.PrizeRule) models.MatchResult {
|
||||
mainMatches := countMatches(ticket.Balls, draw.Balls)
|
||||
bonusMatches := countMatches(ticket.BonusBalls, draw.BonusBalls)
|
||||
|
||||
prizeTier := getPrizeTier(ticket.GameType, mainMatches, bonusMatches, rules)
|
||||
isWinner := prizeTier != ""
|
||||
|
||||
return models.MatchResult{
|
||||
MatchedDrawID: draw.DrawID,
|
||||
MatchedMain: mainMatches,
|
||||
MatchedBonus: bonusMatches,
|
||||
PrizeTier: prizeTier,
|
||||
IsWinner: isWinner,
|
||||
}
|
||||
}
|
||||
|
||||
func countMatches(a, b []int) int {
|
||||
m := make(map[int]bool)
|
||||
for _, n := range b {
|
||||
m[n] = true
|
||||
}
|
||||
match := 0
|
||||
for _, n := range a {
|
||||
if m[n] {
|
||||
match++
|
||||
}
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
func getPrizeTier(game string, main, bonus int, rules []models.PrizeRule) string {
|
||||
for _, rule := range rules {
|
||||
if rule.Game == game && rule.MainMatches == main && rule.BonusMatches == bonus {
|
||||
return rule.Tier
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
186
internal/handlers/messages.go
Normal file
186
internal/handlers/messages.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
templateHandlers "synlotto-website/handlers/template"
|
||||
"synlotto-website/helpers"
|
||||
httpHelpers "synlotto-website/helpers/http"
|
||||
securityHelpers "synlotto-website/helpers/security"
|
||||
templateHelpers "synlotto-website/helpers/template"
|
||||
|
||||
"synlotto-website/storage"
|
||||
)
|
||||
|
||||
func MessagesInboxHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
templateHelpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
page := helpers.Atoi(r.URL.Query().Get("page"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
perPage := 10
|
||||
|
||||
totalCount := storage.GetInboxMessageCount(db, userID)
|
||||
totalPages := (totalCount + perPage - 1) / perPage
|
||||
if totalPages == 0 {
|
||||
totalPages = 1
|
||||
}
|
||||
|
||||
messages := storage.GetInboxMessages(db, userID, page, perPage)
|
||||
|
||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
|
||||
context["Messages"] = messages
|
||||
context["CurrentPage"] = page
|
||||
context["TotalPages"] = totalPages
|
||||
context["PageRange"] = templateHelpers.PageRange(page, totalPages)
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("messages.html", "templates/account/messages/index.html")
|
||||
|
||||
if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil {
|
||||
// ToDo: Make this load all error pages without defining explictly.
|
||||
templateHelpers.RenderError(w, r, 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ReadMessageHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := r.URL.Query().Get("id")
|
||||
messageID := helpers.Atoi(idStr)
|
||||
|
||||
session, _ := httpHelpers.GetSession(w, r)
|
||||
userID, ok := session.Values["user_id"].(int)
|
||||
if !ok {
|
||||
templateHelpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
message, err := storage.GetMessageByID(db, userID, messageID)
|
||||
if err != nil {
|
||||
log.Printf("❌ Message not found: %v", err)
|
||||
message = nil
|
||||
} else if !message.IsRead {
|
||||
_ = storage.MarkMessageAsRead(db, messageID, userID)
|
||||
}
|
||||
|
||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
context["Message"] = message
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("read-message.html", "templates/account/messages/read.html")
|
||||
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
}
|
||||
}
|
||||
|
||||
func ArchiveMessageHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := helpers.Atoi(r.URL.Query().Get("id"))
|
||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
templateHelpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
err := storage.ArchiveMessage(db, userID, id)
|
||||
if err != nil {
|
||||
templateHelpers.SetFlash(w, r, "Failed to archive message.")
|
||||
} else {
|
||||
templateHelpers.SetFlash(w, r, "Message archived.")
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/account/messages", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func ArchivedMessagesHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
templateHelpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
page := helpers.Atoi(r.URL.Query().Get("page"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
perPage := 10
|
||||
|
||||
messages := storage.GetArchivedMessages(db, userID, page, perPage)
|
||||
hasMore := len(messages) == perPage
|
||||
|
||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
context["Messages"] = messages
|
||||
context["Page"] = page
|
||||
context["HasMore"] = hasMore
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("archived.html", "templates/account/messages/archived.html")
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
}
|
||||
}
|
||||
|
||||
func SendMessageHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
tmpl := templateHelpers.LoadTemplateFiles("send-message.html", "templates/account/messages/send.html")
|
||||
|
||||
if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil {
|
||||
templateHelpers.RenderError(w, r, 500)
|
||||
}
|
||||
case http.MethodPost:
|
||||
senderID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
templateHelpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
recipientID := helpers.Atoi(r.FormValue("recipient_id"))
|
||||
subject := r.FormValue("subject")
|
||||
body := r.FormValue("message")
|
||||
|
||||
if err := storage.SendMessage(db, senderID, recipientID, subject, body); err != nil {
|
||||
templateHelpers.SetFlash(w, r, "Failed to send message.")
|
||||
} else {
|
||||
templateHelpers.SetFlash(w, r, "Message sent.")
|
||||
}
|
||||
http.Redirect(w, r, "/account/messages", http.StatusSeeOther)
|
||||
default:
|
||||
templateHelpers.RenderError(w, r, 405)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RestoreMessageHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := helpers.Atoi(r.URL.Query().Get("id"))
|
||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
templateHelpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
err := storage.RestoreMessage(db, userID, id)
|
||||
if err != nil {
|
||||
templateHelpers.SetFlash(w, r, "Failed to restore message.")
|
||||
} else {
|
||||
templateHelpers.SetFlash(w, r, "Message restored.")
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/account/messages/archived", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
70
internal/handlers/notifications.go
Normal file
70
internal/handlers/notifications.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
templateHandlers "synlotto-website/handlers/template"
|
||||
httpHelpers "synlotto-website/helpers/http"
|
||||
templateHelpers "synlotto-website/helpers/template"
|
||||
|
||||
"synlotto-website/storage"
|
||||
)
|
||||
|
||||
func NotificationsHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("index.html", "templates/account/notifications/index.html")
|
||||
|
||||
err := tmpl.ExecuteTemplate(w, "layout", context)
|
||||
if err != nil {
|
||||
log.Println("❌ Template render error:", err)
|
||||
http.Error(w, "Error rendering notifications page", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func MarkNotificationReadHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
notificationIDStr := r.URL.Query().Get("id")
|
||||
notificationID, err := strconv.Atoi(notificationIDStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid notification ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
session, _ := httpHelpers.GetSession(w, r)
|
||||
userID, ok := session.Values["user_id"].(int)
|
||||
if !ok {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
notification, err := storage.GetNotificationByID(db, userID, notificationID)
|
||||
if err != nil {
|
||||
log.Printf("❌ Notification not found or belongs to another user: %v", err)
|
||||
notification = nil
|
||||
} else if !notification.IsRead {
|
||||
err = storage.MarkNotificationAsRead(db, userID, notificationID)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Failed to mark as read: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
context["Notification"] = notification
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("read.html", "templates/account/notifications/read.html")
|
||||
|
||||
err = tmpl.ExecuteTemplate(w, "layout", context)
|
||||
if err != nil {
|
||||
log.Printf("❌ Template render error: %v", err)
|
||||
http.Error(w, "Template render error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
137
internal/handlers/results.go
Normal file
137
internal/handlers/results.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
templateHelpers "synlotto-website/helpers/template"
|
||||
|
||||
"synlotto-website/helpers"
|
||||
"synlotto-website/middleware"
|
||||
"synlotto-website/models"
|
||||
)
|
||||
|
||||
func ResultsThunderball(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
limiter := middleware.GetVisitorLimiter(ip)
|
||||
|
||||
if !limiter.Allow() {
|
||||
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
const pageSize = 20
|
||||
page := 1
|
||||
offset := 0
|
||||
|
||||
query := r.URL.Query().Get("q")
|
||||
pageStr := r.URL.Query().Get("page")
|
||||
yearFilter := r.URL.Query().Get("year")
|
||||
machineFilter := r.URL.Query().Get("machine")
|
||||
ballSetFilter := r.URL.Query().Get("ballset")
|
||||
|
||||
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
|
||||
page = p
|
||||
offset = (page - 1) * pageSize
|
||||
}
|
||||
|
||||
isValidDate := regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`).MatchString
|
||||
isValidNumber := regexp.MustCompile(`^\d+$`).MatchString
|
||||
doSearch := isValidDate(query) || isValidNumber(query)
|
||||
|
||||
whereClause := "WHERE 1=1"
|
||||
args := []interface{}{}
|
||||
|
||||
if doSearch {
|
||||
whereClause += " AND (draw_date = ? OR id = ?)"
|
||||
args = append(args, query, query)
|
||||
}
|
||||
if yearFilter != "" {
|
||||
whereClause += " AND strftime('%Y', draw_date) = ?"
|
||||
args = append(args, yearFilter)
|
||||
}
|
||||
if machineFilter != "" {
|
||||
whereClause += " AND machine = ?"
|
||||
args = append(args, machineFilter)
|
||||
}
|
||||
if ballSetFilter != "" {
|
||||
whereClause += " AND ballset = ?"
|
||||
args = append(args, ballSetFilter)
|
||||
}
|
||||
|
||||
totalPages, totalResults := templateHelpers.GetTotalPages(db, "results_thunderball", whereClause, args, pageSize)
|
||||
if page < 1 || page > totalPages {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
querySQL := `
|
||||
SELECT id, draw_date, machine, ballset, ball1, ball2, ball3, ball4, ball5, thunderball
|
||||
FROM results_thunderball
|
||||
` + whereClause + `
|
||||
ORDER BY id DESC
|
||||
LIMIT ? OFFSET ?`
|
||||
argsWithLimit := append(args, pageSize, offset)
|
||||
|
||||
rows, err := db.Query(querySQL, argsWithLimit...)
|
||||
if err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
log.Println("❌ DB error:", err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var results []models.ThunderballResult
|
||||
for rows.Next() {
|
||||
var res models.ThunderballResult
|
||||
if err := rows.Scan(
|
||||
&res.Id, &res.DrawDate, &res.Machine, &res.BallSet,
|
||||
&res.Ball1, &res.Ball2, &res.Ball3, &res.Ball4, &res.Ball5, &res.Thunderball,
|
||||
); 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)
|
||||
}
|
||||
|
||||
years, _ := helpers.GetDistinctValues(db, "strftime('%Y', draw_date)")
|
||||
machines, _ := helpers.GetDistinctValues(db, "machine")
|
||||
ballsets, _ := helpers.GetDistinctValues(db, "ballset")
|
||||
|
||||
var noResultsMsg string
|
||||
if query != "" && !doSearch {
|
||||
noResultsMsg = "Invalid search. Please enter a draw number or date (yyyy-mm-dd)."
|
||||
} else if len(results) == 0 && query != "" {
|
||||
noResultsMsg = "No results found for \"" + query + "\""
|
||||
}
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("thunderball.html", "templates/results/thunderball.html")
|
||||
|
||||
err = tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{
|
||||
"Results": results,
|
||||
"Page": page,
|
||||
"TotalPages": totalPages,
|
||||
"TotalResults": totalResults,
|
||||
"Query": query,
|
||||
"NoResultsMsg": noResultsMsg,
|
||||
"Years": years,
|
||||
"Machines": machines,
|
||||
"BallSets": ballsets,
|
||||
"YearFilter": yearFilter,
|
||||
"MachineFilter": machineFilter,
|
||||
"BallSetFilter": ballSetFilter,
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("❌ Template error:", err)
|
||||
http.Error(w, "Error rendering results", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
23
internal/handlers/session/account.go
Normal file
23
internal/handlers/session/account.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
var (
|
||||
SessionStore *sessions.CookieStore
|
||||
Name string
|
||||
)
|
||||
|
||||
func GetSession(w http.ResponseWriter, r *http.Request) (*sessions.Session, error) {
|
||||
if SessionStore == nil {
|
||||
return nil, fmt.Errorf("session store not initialized")
|
||||
}
|
||||
if Name == "" {
|
||||
return nil, fmt.Errorf("session name not configured")
|
||||
}
|
||||
return SessionStore.Get(r, Name)
|
||||
}
|
||||
32
internal/handlers/session/auth.go
Normal file
32
internal/handlers/session/auth.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
)
|
||||
|
||||
var (
|
||||
authKey []byte
|
||||
encryptKey []byte
|
||||
)
|
||||
|
||||
func SecureCookie(w http.ResponseWriter, name, value string, isProduction bool) error {
|
||||
s := securecookie.New(authKey, encryptKey)
|
||||
|
||||
encoded, err := s.Encode(name, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: name,
|
||||
Value: encoded,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: isProduction,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
36
internal/handlers/statistics/thunderball.go
Normal file
36
internal/handlers/statistics/thunderball.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
templateHandlers "synlotto-website/handlers/template"
|
||||
templateHelpers "synlotto-website/helpers/template"
|
||||
|
||||
"synlotto-website/middleware"
|
||||
)
|
||||
|
||||
func StatisticsThunderball(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
limiter := middleware.GetVisitorLimiter(ip)
|
||||
|
||||
if !limiter.Allow() {
|
||||
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
||||
context := templateHelpers.TemplateContext(w, r, data)
|
||||
|
||||
tmpl := templateHelpers.LoadTemplateFiles("statistics.html", "templates/statistics/thunderball.html")
|
||||
|
||||
err := tmpl.ExecuteTemplate(w, "layout", context)
|
||||
if err != nil {
|
||||
log.Println("❌ Template render error:", err)
|
||||
http.Error(w, "Error rendering homepage", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
46
internal/handlers/template/templatedata.go
Normal file
46
internal/handlers/template/templatedata.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
httpHelper "synlotto-website/helpers/http"
|
||||
|
||||
"synlotto-website/models"
|
||||
"synlotto-website/storage"
|
||||
)
|
||||
|
||||
func BuildTemplateData(db *sql.DB, w http.ResponseWriter, r *http.Request) models.TemplateData {
|
||||
session, err := httpHelper.GetSession(w, r)
|
||||
if err != nil {
|
||||
log.Printf("Session error: %v", err)
|
||||
}
|
||||
|
||||
var user *models.User
|
||||
var isAdmin bool
|
||||
var notificationCount int
|
||||
var notifications []models.Notification
|
||||
var messageCount int
|
||||
var messages []models.Message
|
||||
|
||||
if userId, ok := session.Values["user_id"].(int); ok {
|
||||
user = storage.GetUserByID(db, userId)
|
||||
if user != nil {
|
||||
isAdmin = user.IsAdmin
|
||||
notificationCount = storage.GetNotificationCount(db, user.Id)
|
||||
notifications = storage.GetRecentNotifications(db, user.Id, 15)
|
||||
messageCount, _ = storage.GetMessageCount(db, user.Id)
|
||||
messages = storage.GetRecentMessages(db, user.Id, 15)
|
||||
}
|
||||
}
|
||||
|
||||
return models.TemplateData{
|
||||
User: user,
|
||||
IsAdmin: isAdmin,
|
||||
NotificationCount: notificationCount,
|
||||
Notifications: notifications,
|
||||
MessageCount: messageCount,
|
||||
Messages: messages,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user