Compare commits

...

9 Commits

Author SHA1 Message Date
0f60be448d Refactoring continues. 2025-10-23 21:51:15 +01:00
82f457c5a4 New packages for MySQL. 2025-10-23 19:53:06 +01:00
b098915ab9 Update import paths 2025-10-23 19:51:28 +01:00
21ebc9c34b Refactor and remove sqlite and replace with MySQL 2025-10-23 18:43:31 +01:00
d53e27eea8 Switching to MySQL 2025-10-22 22:43:35 +01:00
752db0b89d New statistics related models and handlers. 2025-10-22 20:58:25 +01:00
8d06a7a962 Remove log output and updated comment 2025-10-22 20:57:03 +01:00
7597fff8b1 routes.SetupStatisticsRoutes 2025-10-22 20:56:04 +01:00
58dd313703 Forgot to add draw IDs to tables. 2025-10-20 14:48:33 +01:00
141 changed files with 1507 additions and 644 deletions

5
go.mod
View File

@@ -11,9 +11,14 @@ require (
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/golang-migrate/migrate/v4 v4.19.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect

13
go.sum
View File

@@ -1,5 +1,11 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
@@ -12,6 +18,11 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
@@ -24,6 +35,7 @@ golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtD
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -33,6 +45,7 @@ golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo= modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=

View File

@@ -6,13 +6,13 @@ import (
"net/http" "net/http"
"time" "time"
httpHelpers "synlotto-website/helpers/http" httpHelpers "synlotto-website/internal/helpers/http"
securityHelpers "synlotto-website/helpers/security" securityHelpers "synlotto-website/internal/helpers/security"
templateHelpers "synlotto-website/helpers/template" templateHelpers "synlotto-website/internal/helpers/template"
"synlotto-website/logging" "synlotto-website/internal/logging"
"synlotto-website/models" "synlotto-website/internal/models"
"synlotto-website/storage" "synlotto-website/internal/storage"
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
) )
@@ -41,6 +41,7 @@ func Login(db *sql.DB) http.HandlerFunc {
username := r.FormValue("username") username := r.FormValue("username")
password := r.FormValue("password") password := r.FormValue("password")
// ToDo: this outputs password in clear text remove or obscure!
logging.Info("🔐 Login attempt - Username: %s, Password: %s", username, password) logging.Info("🔐 Login attempt - Username: %s, Password: %s", username, password)
user := storage.GetUserByUsername(db, username) user := storage.GetUserByUsername(db, username)
@@ -51,7 +52,6 @@ func Login(db *sql.DB) http.HandlerFunc {
session, _ := httpHelpers.GetSession(w, r) session, _ := httpHelpers.GetSession(w, r)
session.Values["flash"] = "Invalid username or password." session.Values["flash"] = "Invalid username or password."
session.Save(r, w) session.Save(r, w)
log.Printf("login did it")
http.Redirect(w, r, "/account/login", http.StatusSeeOther) http.Redirect(w, r, "/account/login", http.StatusSeeOther)
return return
} }

View File

@@ -5,10 +5,10 @@ import (
"log" "log"
"net/http" "net/http"
templateHelpers "synlotto-website/helpers/template" templateHelpers "synlotto-website/internal/helpers/template"
"synlotto-website/middleware" "synlotto-website/internal/http/middleware"
"synlotto-website/models" "synlotto-website/internal/models"
) )
type AdminLogEntry struct { type AdminLogEntry struct {

View File

@@ -5,12 +5,12 @@ import (
"log" "log"
"net/http" "net/http"
httpHelpers "synlotto-website/helpers/http" httpHelpers "synlotto-website/internal/helpers/http"
securityHelpers "synlotto-website/helpers/security" securityHelpers "synlotto-website/internal/helpers/security"
templateHelpers "synlotto-website/helpers/template" templateHelpers "synlotto-website/internal/helpers/template"
"synlotto-website/models" "synlotto-website/internal/models"
"synlotto-website/storage" "synlotto-website/internal/storage"
) )
var ( var (

View File

@@ -5,10 +5,10 @@ import (
"log" "log"
"net/http" "net/http"
httpHelpers "synlotto-website/helpers/http" httpHelpers "synlotto-website/internal/helpers/http"
templateHelpers "synlotto-website/helpers/template" templateHelpers "synlotto-website/internal/helpers/template"
"synlotto-website/models" "synlotto-website/internal/models"
) )
func NewDrawHandler(db *sql.DB) http.HandlerFunc { func NewDrawHandler(db *sql.DB) http.HandlerFunc {

View File

@@ -8,10 +8,10 @@ import (
"net/url" "net/url"
"strconv" "strconv"
templateHelpers "synlotto-website/helpers/template" templateHelpers "synlotto-website/internal/helpers/template"
services "synlotto-website/services/tickets" services "synlotto-website/internal/services/tickets"
"synlotto-website/models" "synlotto-website/internal/models"
) )
func AdminTriggersHandler(db *sql.DB) http.HandlerFunc { func AdminTriggersHandler(db *sql.DB) http.HandlerFunc {

View File

@@ -6,10 +6,10 @@ import (
"net/http" "net/http"
"strconv" "strconv"
httpHelpers "synlotto-website/helpers/http" httpHelpers "synlotto-website/internal/helpers/http"
templateHelpers "synlotto-website/helpers/template" templateHelpers "synlotto-website/internal/helpers/template"
"synlotto-website/models" "synlotto-website/internal/models"
) )
func AddPrizesHandler(db *sql.DB) http.HandlerFunc { func AddPrizesHandler(db *sql.DB) http.HandlerFunc {

View File

@@ -1,7 +1,7 @@
package handlers package handlers
import ( import (
"synlotto-website/models" "synlotto-website/internal/models"
) )
var Draws []models.ThunderballResult var Draws []models.ThunderballResult

View File

@@ -5,8 +5,8 @@ import (
"log" "log"
"net/http" "net/http"
templateHandlers "synlotto-website/handlers/template" templateHandlers "synlotto-website/internal/handlers/template"
templateHelpers "synlotto-website/helpers/template" templateHelpers "synlotto-website/internal/helpers/template"
) )
func Home(db *sql.DB) http.HandlerFunc { func Home(db *sql.DB) http.HandlerFunc {

View File

@@ -5,11 +5,11 @@ import (
"log" "log"
"net/http" "net/http"
templateHelpers "synlotto-website/helpers/template" templateHelpers "synlotto-website/internal/helpers/template"
"synlotto-website/helpers" "synlotto-website/internal/helpers"
"synlotto-website/models" "synlotto-website/internal/models"
"synlotto-website/storage" "synlotto-website/internal/storage"
) )
func NewDraw(db *sql.DB) http.HandlerFunc { func NewDraw(db *sql.DB) http.HandlerFunc {

View File

@@ -6,13 +6,13 @@ import (
"log" "log"
"net/http" "net/http"
templateHandlers "synlotto-website/handlers/template" templateHandlers "synlotto-website/internal/handlers/template"
securityHelpers "synlotto-website/helpers/security" securityHelpers "synlotto-website/internal/helpers/security"
templateHelpers "synlotto-website/helpers/template" templateHelpers "synlotto-website/internal/helpers/template"
"synlotto-website/helpers" "synlotto-website/internal/helpers"
"synlotto-website/models" "synlotto-website/internal/models"
"synlotto-website/storage" "synlotto-website/internal/storage"
) )
func CreateSyndicateHandler(db *sql.DB) http.HandlerFunc { func CreateSyndicateHandler(db *sql.DB) http.HandlerFunc {

View File

@@ -7,12 +7,12 @@ import (
"strconv" "strconv"
"time" "time"
templateHandlers "synlotto-website/handlers/template" templateHandlers "synlotto-website/internal/handlers/template"
securityHelpers "synlotto-website/helpers/security" securityHelpers "synlotto-website/internal/helpers/security"
templateHelpers "synlotto-website/helpers/template" templateHelpers "synlotto-website/internal/helpers/template"
"synlotto-website/helpers" "synlotto-website/internal/helpers"
"synlotto-website/storage" "synlotto-website/internal/storage"
) )
func SyndicateInviteHandler(db *sql.DB) http.HandlerFunc { func SyndicateInviteHandler(db *sql.DB) http.HandlerFunc {

View File

@@ -10,13 +10,13 @@ import (
"strconv" "strconv"
"time" "time"
httpHelpers "synlotto-website/helpers/http" httpHelpers "synlotto-website/internal/helpers/http"
securityHelpers "synlotto-website/helpers/security" securityHelpers "synlotto-website/internal/helpers/security"
templateHelpers "synlotto-website/helpers/template" templateHelpers "synlotto-website/internal/helpers/template"
draws "synlotto-website/services/draws" draws "synlotto-website/internal/services/draws"
"synlotto-website/helpers" "synlotto-website/internal/helpers"
"synlotto-website/models" "synlotto-website/internal/models"
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
) )

View File

@@ -1,7 +1,7 @@
package handlers package handlers
import ( import (
"synlotto-website/models" "synlotto-website/internal/models"
) )
func MatchTicketToDraw(ticket models.MatchTicket, draw models.DrawResult, rules []models.PrizeRule) models.MatchResult { func MatchTicketToDraw(ticket models.MatchTicket, draw models.DrawResult, rules []models.PrizeRule) models.MatchResult {

View File

@@ -5,13 +5,13 @@ import (
"log" "log"
"net/http" "net/http"
templateHandlers "synlotto-website/handlers/template" templateHandlers "synlotto-website/internal/handlers/template"
"synlotto-website/helpers" httpHelpers "synlotto-website/internal/helpers/http"
httpHelpers "synlotto-website/helpers/http" securityHelpers "synlotto-website/internal/helpers/security"
securityHelpers "synlotto-website/helpers/security" templateHelpers "synlotto-website/internal/helpers/template"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/storage" "synlotto-website/internal/helpers"
storage "synlotto-website/internal/storage/mysql"
) )
func MessagesInboxHandler(db *sql.DB) http.HandlerFunc { func MessagesInboxHandler(db *sql.DB) http.HandlerFunc {

View File

@@ -6,11 +6,11 @@ import (
"net/http" "net/http"
"strconv" "strconv"
templateHandlers "synlotto-website/handlers/template" templateHandlers "synlotto-website/internal/handlers/template"
httpHelpers "synlotto-website/helpers/http" httpHelpers "synlotto-website/internal/helpers/http"
templateHelpers "synlotto-website/helpers/template" templateHelpers "synlotto-website/internal/helpers/template"
"synlotto-website/storage" "synlotto-website/internal/storage"
) )
func NotificationsHandler(db *sql.DB) http.HandlerFunc { func NotificationsHandler(db *sql.DB) http.HandlerFunc {

View File

@@ -9,11 +9,11 @@ import (
"sort" "sort"
"strconv" "strconv"
templateHelpers "synlotto-website/helpers/template" templateHelpers "synlotto-website/internal/helpers/template"
"synlotto-website/helpers" "synlotto-website/internal/helpers"
"synlotto-website/middleware" "synlotto-website/internal/http/middleware"
"synlotto-website/models" "synlotto-website/internal/models"
) )
func ResultsThunderball(db *sql.DB) http.HandlerFunc { func ResultsThunderball(db *sql.DB) http.HandlerFunc {

View File

@@ -0,0 +1,36 @@
package handlers
import (
"database/sql"
"log"
"net"
"net/http"
templateHandlers "synlotto-website/internal/handlers/template"
templateHelpers "synlotto-website/internal/helpers/template"
"synlotto-website/internal/http/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)
}
}
}

View File

@@ -5,10 +5,10 @@ import (
"log" "log"
"net/http" "net/http"
httpHelper "synlotto-website/helpers/http" httpHelper "synlotto-website/internal/helpers/http"
"synlotto-website/models" "synlotto-website/internal/models"
"synlotto-website/storage" "synlotto-website/internal/storage"
) )
func BuildTemplateData(db *sql.DB, w http.ResponseWriter, r *http.Request) models.TemplateData { func BuildTemplateData(db *sql.DB, w http.ResponseWriter, r *http.Request) models.TemplateData {

View File

@@ -2,7 +2,7 @@ package helpers
import ( import (
"database/sql" "database/sql"
"synlotto-website/models" "synlotto-website/internal/models"
) )
func BuildBallsSlice(t models.Ticket) []int { func BuildBallsSlice(t models.Ticket) []int {

View File

@@ -4,9 +4,9 @@ import (
"net/http" "net/http"
"time" "time"
session "synlotto-website/handlers/session" session "synlotto-website/internal/handlers/session"
"synlotto-website/constants" "synlotto-website/internal/platform/constants"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
) )

View File

@@ -3,7 +3,7 @@ package security
import ( import (
"net/http" "net/http"
httpHelpers "synlotto-website/helpers/http" httpHelpers "synlotto-website/internal/helpers/http"
) )
func GetCurrentUserID(r *http.Request) (int, bool) { func GetCurrentUserID(r *http.Request) (int, bool) {

View File

@@ -7,9 +7,9 @@ import (
"strings" "strings"
"time" "time"
"synlotto-website/config" helpers "synlotto-website/internal/helpers/http"
helpers "synlotto-website/helpers/http" "synlotto-website/internal/models"
"synlotto-website/models" "synlotto-website/internal/platform/config"
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
) )

View File

@@ -6,7 +6,7 @@ import (
"net/http" "net/http"
"os" "os"
"synlotto-website/models" "synlotto-website/internal/models"
) )
func RenderError(w http.ResponseWriter, r *http.Request, statusCode int) { func RenderError(w http.ResponseWriter, r *http.Request, statusCode int) {

View File

@@ -4,9 +4,9 @@ import (
"net/http" "net/http"
"time" "time"
httpHelpers "synlotto-website/helpers/http" httpHelpers "synlotto-website/internal/helpers/http"
"synlotto-website/constants" "synlotto-website/internal/platform/constants"
) )
func Auth(required bool) func(http.HandlerFunc) http.HandlerFunc { func Auth(required bool) func(http.HandlerFunc) http.HandlerFunc {

View File

@@ -5,7 +5,7 @@ import (
"net/http" "net/http"
"runtime/debug" "runtime/debug"
templateHelpers "synlotto-website/helpers/template" templateHelpers "synlotto-website/internal/helpers/template"
) )
func Recover(next http.Handler) http.Handler { func Recover(next http.Handler) http.Handler {

View File

@@ -5,9 +5,9 @@ import (
"net/http" "net/http"
"time" "time"
session "synlotto-website/handlers/session" session "synlotto-website/internal/handlers/session"
"synlotto-website/constants" "synlotto-website/internal/platform/constants"
) )
func SessionTimeout(next http.HandlerFunc) http.HandlerFunc { func SessionTimeout(next http.HandlerFunc) http.HandlerFunc {

View File

@@ -4,11 +4,11 @@ import (
"database/sql" "database/sql"
"net/http" "net/http"
accountHandlers "synlotto-website/handlers/account" accountHandlers "synlotto-website/internal/handlers/account"
lotteryDrawHandlers "synlotto-website/handlers/lottery/tickets" lotteryDrawHandlers "synlotto-website/internal/handlers/lottery/tickets"
"synlotto-website/handlers" "synlotto-website/internal/handlers"
"synlotto-website/middleware" "synlotto-website/internal/http/middleware"
) )
func SetupAccountRoutes(mux *http.ServeMux, db *sql.DB) { func SetupAccountRoutes(mux *http.ServeMux, db *sql.DB) {

View File

@@ -4,8 +4,8 @@ import (
"database/sql" "database/sql"
"net/http" "net/http"
admin "synlotto-website/handlers/admin" admin "synlotto-website/internal/handlers/admin"
"synlotto-website/middleware" "synlotto-website/internal/http/middleware"
) )
func SetupAdminRoutes(mux *http.ServeMux, db *sql.DB) { func SetupAdminRoutes(mux *http.ServeMux, db *sql.DB) {

View File

@@ -4,7 +4,7 @@ import (
"database/sql" "database/sql"
"net/http" "net/http"
"synlotto-website/handlers" "synlotto-website/internal/handlers"
) )
func SetupResultRoutes(mux *http.ServeMux, db *sql.DB) { func SetupResultRoutes(mux *http.ServeMux, db *sql.DB) {

View File

@@ -0,0 +1,13 @@
package routes
import (
"database/sql"
"net/http"
handlers "synlotto-website/internal/handlers/statistics"
"synlotto-website/internal/http/middleware"
)
func SetupStatisticsRoutes(mux *http.ServeMux, db *sql.DB) {
mux.HandleFunc("/statistics/thunderball", middleware.Auth(true)(handlers.StatisticsThunderball(db)))
}

View File

@@ -4,9 +4,9 @@ import (
"database/sql" "database/sql"
"net/http" "net/http"
lotterySyndicateHandlers "synlotto-website/handlers/lottery/syndicate" lotterySyndicateHandlers "synlotto-website/internal/handlers/lottery/syndicate"
"synlotto-website/middleware" "synlotto-website/internal/http/middleware"
) )
func SetupSyndicateRoutes(mux *http.ServeMux, db *sql.DB) { func SetupSyndicateRoutes(mux *http.ServeMux, db *sql.DB) {

View File

@@ -4,7 +4,7 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"synlotto-website/models" "synlotto-website/internal/models"
) )
func LogConfig(config *models.Config) { func LogConfig(config *models.Config) {

View File

@@ -5,6 +5,17 @@ type Config struct {
CSRFKey string `json:"csrfKey"` CSRFKey string `json:"csrfKey"`
} `json:"csrf"` } `json:"csrf"`
Database struct {
Server string `json:"server"`
Port int `json:"port"`
DatabaseNamed string `json:"databaseName"`
MaxOpenConnections int `json:"maxOpenConnections"`
MaxIdleConnections int `json:"maxIdleConnections"`
ConnectionMaxLifetime string `json:"connectionMaxLifetime"`
Username string `json:"username"`
Password string `json:"password"`
}
HttpServer struct { HttpServer struct {
Port int `json:"port"` Port int `json:"port"`
Address string `json:"address"` Address string `json:"address"`

View File

@@ -0,0 +1,9 @@
package models
type MachineUsage struct {
Machine string
DrawsUsed int
PctOfDraws float64
FirstUsed string
LastUsed string
}

View File

@@ -0,0 +1,15 @@
package models
import (
"database/sql"
)
type NextMachineBallsetPrediction struct {
NextDrawDate string
CurrentMachine string
EstimatedNextMachine string
MachineTransitionPct float64
CurrentBallset sql.NullString
EstimatedNextBallset sql.NullString
BallsetTransitionPct sql.NullFloat64
}

View File

@@ -0,0 +1,18 @@
package models
type TopNum struct {
Number int
Frequency int
}
type Pair struct {
A int
B int
Frequency int
}
type ZScore struct {
Ball int
Recent int
Z float64
}

34
internal/models/user.go Normal file
View File

@@ -0,0 +1,34 @@
package models
import (
"time"
)
type User struct {
Id int
Username string
PasswordHash string
IsAdmin bool
}
// ToDo: should be in a notification model?
type Notification struct {
ID int
UserId int
Subject string
Body string
IsRead bool
CreatedAt time.Time
}
// ToDo: should be in a message model?
type Message struct {
ID int
SenderId int
RecipientId int
Subject string
Message string
IsRead bool
CreatedAt time.Time
ArchivedAt *time.Time
}

View File

@@ -5,7 +5,7 @@ import (
"time" "time"
internal "synlotto-website/internal/licensecheck" internal "synlotto-website/internal/licensecheck"
"synlotto-website/models" "synlotto-website/internal/models"
) )
var globalChecker *internal.LicenseChecker var globalChecker *internal.LicenseChecker

View File

@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"synlotto-website/models" "synlotto-website/internal/models"
) )
type AppState struct { type AppState struct {

View File

@@ -10,11 +10,11 @@ import (
"os" "os"
"time" "time"
sessionHandlers "synlotto-website/handlers/session" sessionHandlers "synlotto-website/internal/handlers/session"
sessionHelpers "synlotto-website/helpers/session" sessionHelpers "synlotto-website/internal/helpers/session"
"synlotto-website/logging" "synlotto-website/internal/logging"
"synlotto-website/models" "synlotto-website/internal/models"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
) )

View File

@@ -3,7 +3,7 @@ package config
import ( import (
"sync" "sync"
"synlotto-website/models" "synlotto-website/internal/models"
) )
var ( var (

View File

@@ -0,0 +1,43 @@
package rules
import (
"synlotto-website/internal/models"
"synlotto-website/internal/rules"
)
var ThunderballPrizeRules = []models.PrizeRule{
{Game: rules.GameThunderball, MainMatches: 0, BonusMatches: 1, Tier: "Tier 1"},
{Game: rules.GameThunderball, MainMatches: 1, BonusMatches: 1, Tier: "Tier 2"},
{Game: rules.GameThunderball, MainMatches: 2, BonusMatches: 1, Tier: "Tier 3"},
{Game: rules.GameThunderball, MainMatches: 3, BonusMatches: 0, Tier: "Tier 4"},
{Game: rules.GameThunderball, MainMatches: 3, BonusMatches: 1, Tier: "Tier 5"},
{Game: rules.GameThunderball, MainMatches: 4, BonusMatches: 0, Tier: "Tier 6"},
{Game: rules.GameThunderball, MainMatches: 4, BonusMatches: 1, Tier: "Tier 7"},
{Game: rules.GameThunderball, MainMatches: 5, BonusMatches: 0, Tier: "Second"},
{Game: rules.GameThunderball, MainMatches: 5, BonusMatches: 1, Tier: "Jackpot"},
}
func GetThunderballPrizeIndex(main, bonus int) (int, bool) {
switch {
case main == 0 && bonus == 1:
return 9, true
case main == 1 && bonus == 1:
return 8, true
case main == 2 && bonus == 1:
return 7, true
case main == 3 && bonus == 0:
return 6, true
case main == 3 && bonus == 1:
return 5, true
case main == 4 && bonus == 0:
return 4, true
case main == 4 && bonus == 1:
return 3, true
case main == 5 && bonus == 0:
return 2, true
case main == 5 && bonus == 1:
return 1, true
default:
return 0, false
}
}

23
internal/rules/types.go Normal file
View File

@@ -0,0 +1,23 @@
package rules
// PrizeRule defines how many matches correspond to which prize tier.
type PrizeRule struct {
Game string
MainMatches int
BonusMatches int
Tier string
}
// ToDo: should this struct not be in a model~?
// PrizeInfo describes the tier and payout details.
type PrizeInfo struct {
Tier string
Amount float64
Label string
}
// Game names (use constants to avoid typos). ToDo: should this not be in my constants folder or avoid constands folder as it may end up as a junk draw
const (
GameThunderball = "Thunderball"
GameEuroJackpot = "EuroJackpot"
)

View File

@@ -3,7 +3,7 @@ package services
import ( import (
"database/sql" "database/sql"
"log" "log"
"synlotto-website/models" "synlotto-website/internal/models"
) )
func GetDrawResultForTicket(db *sql.DB, game string, drawDate string) models.DrawResult { func GetDrawResultForTicket(db *sql.DB, game string, drawDate string) models.DrawResult {

View File

@@ -1,11 +1,11 @@
package matcher package services
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"synlotto-website/helpers" "synlotto-website/internal/helpers"
"synlotto-website/models" "synlotto-website/internal/models"
thunderballRules "synlotto-website/rules" thunderballRules "synlotto-website/internal/rules/thunderball"
) )
func MatchTicketToDraw(ticket models.MatchTicket, draw models.DrawResult, rules []models.PrizeRule, db *sql.DB) models.MatchResult { func MatchTicketToDraw(ticket models.MatchTicket, draw models.DrawResult, rules []models.PrizeRule, db *sql.DB) models.MatchResult {

View File

@@ -5,13 +5,12 @@ import (
"fmt" "fmt"
"log" "log"
lotteryTicketHandlers "synlotto-website/handlers/lottery/tickets" lotteryTicketHandlers "synlotto-website/internal/handlers/lottery/tickets"
thunderballrules "synlotto-website/rules" thunderballrules "synlotto-website/internal/rules/thunderball"
services "synlotto-website/services/draws" services "synlotto-website/internal/services/draws"
"synlotto-website/helpers" "synlotto-website/internal/helpers"
"synlotto-website/matcher" "synlotto-website/internal/models"
"synlotto-website/models"
) )
func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, error) { func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, error) {

View File

@@ -4,11 +4,13 @@ import (
"database/sql" "database/sql"
"log" "log"
"net/http" "net/http"
"time"
securityHelpers "synlotto-website/helpers/security" securityHelpers "synlotto-website/internal/helpers/security"
templateHelpers "synlotto-website/helpers/template" templateHelpers "synlotto-website/internal/helpers/template"
"synlotto-website/internal/logging"
"synlotto-website/middleware" "synlotto-website/internal/http/middleware"
) )
func AdminOnly(db *sql.DB, next http.HandlerFunc) http.HandlerFunc { func AdminOnly(db *sql.DB, next http.HandlerFunc) http.HandlerFunc {
@@ -38,3 +40,17 @@ func AdminOnly(db *sql.DB, next http.HandlerFunc) http.HandlerFunc {
next(w, r) next(w, r)
}) })
} }
func LogLoginAttempt(r *http.Request, username string, success bool) {
ip := r.RemoteAddr
userAgent := r.UserAgent()
_, err := db.Exec(
`INSERT INTO audit_login (username, success, ip, user_agent, timestamp)
VALUES (?, ?, ?, ?, ?)`,
username, success, ip, userAgent, time.Now().UTC(),
)
if err != nil {
logging.Info("❌ Failed to log login:", err)
}
}

View File

@@ -0,0 +1,78 @@
package storage
import (
"database/sql"
"embed"
"fmt"
"log"
"os"
_ "github.com/go-sql-driver/mysql"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/mysql"
iofs "github.com/golang-migrate/migrate/v4/source/iofs"
)
//go:embed migrations/*.sql
var migrationFiles embed.FS
var DB *sql.DB
// InitDB connects to MySQL, runs migrations, and returns the DB handle.
func InitDB() *sql.DB {
cfg := getDSNFromEnv()
db, err := sql.Open("mysql", cfg)
if err != nil {
log.Fatalf("❌ Failed to connect to MySQL: %v", err)
}
if err := db.Ping(); err != nil {
log.Fatalf("❌ MySQL not reachable: %v", err)
}
if err := runMigrations(db); err != nil {
log.Fatalf("❌ Migration failed: %v", err)
}
DB = db
return db
}
// runMigrations applies any pending .sql files in migrations/
func runMigrations(db *sql.DB) error {
driver, err := mysql.WithInstance(db, &mysql.Config{})
if err != nil {
return err
}
src, err := iofs.New(migrationFiles, "migrations")
if err != nil {
return err
}
m, err := migrate.NewWithInstance("iofs", src, "mysql", driver)
if err != nil {
return err
}
err = m.Up()
if err == migrate.ErrNoChange {
log.Println("✅ Database schema up to date.")
return nil
}
return err
}
func getDSNFromEnv() string {
user := os.Getenv("DB_USER")
pass := os.Getenv("DB_PASS")
host := os.Getenv("DB_HOST") // e.g. localhost or 127.0.0.1
port := os.Getenv("DB_PORT") // e.g. 3306
name := os.Getenv("DB_NAME") // e.g. synlotto
params := "parseTime=true&multiStatements=true"
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?%s",
user, pass, host, port, name, params)
return dsn
}

View File

@@ -0,0 +1,13 @@
package storage
import (
"database/sql"
)
func SendMessage(db *sql.DB, senderID, recipientID int, subject, message string) error {
_, err := db.Exec(`
INSERT INTO users_messages (senderId, recipientId, subject, message)
VALUES (?, ?, ?, ?)
`, senderID, recipientID, subject, message)
return err
}

View File

@@ -0,0 +1,3 @@
// Currently no delete functions, only archiving to remove from user
// view but they can pull them back. Consider a soft delete which hides them from being unarchived for 5 years?
// Then systematically delete after 5 years? or delete sooner but retain backup

View File

@@ -2,8 +2,8 @@ package storage
import ( import (
"database/sql" "database/sql"
"fmt"
"synlotto-website/models" "synlotto-website/internal/models"
) )
func GetMessageCount(db *sql.DB, userID int) (int, error) { func GetMessageCount(db *sql.DB, userID int) (int, error) {
@@ -62,43 +62,6 @@ func GetMessageByID(db *sql.DB, userID, messageID int) (*models.Message, error)
return &m, nil return &m, nil
} }
func MarkMessageAsRead(db *sql.DB, messageID, userID int) error {
result, err := db.Exec(`
UPDATE users_messages
SET is_read = TRUE
WHERE id = ? AND recipientId = ?
`, messageID, userID)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return fmt.Errorf("no matching message found for user_id=%d and message_id=%d", userID, messageID)
}
return nil
}
func ArchiveMessage(db *sql.DB, userID, messageID int) error {
_, err := db.Exec(`
UPDATE users_messages
SET is_archived = TRUE, archived_at = CURRENT_TIMESTAMP
WHERE id = ? AND recipientId = ?
`, messageID, userID)
return err
}
func SendMessage(db *sql.DB, senderID, recipientID int, subject, message string) error {
_, err := db.Exec(`
INSERT INTO users_messages (senderId, recipientId, subject, message)
VALUES (?, ?, ?, ?)
`, senderID, recipientID, subject, message)
return err
}
func GetArchivedMessages(db *sql.DB, userID int, page, perPage int) []models.Message { func GetArchivedMessages(db *sql.DB, userID int, page, perPage int) []models.Message {
offset := (page - 1) * perPage offset := (page - 1) * perPage
rows, err := db.Query(` rows, err := db.Query(`
@@ -167,12 +130,3 @@ func GetInboxMessageCount(db *sql.DB, userID int) int {
} }
return count return count
} }
func RestoreMessage(db *sql.DB, userID, messageID int) error {
_, err := db.Exec(`
UPDATE users_messages
SET is_archived = FALSE, archived_at = NULL
WHERE id = ? AND recipientId = ?
`, messageID, userID)
return err
}

View File

@@ -0,0 +1,44 @@
package storage
import (
"database/sql"
"fmt"
)
func ArchiveMessage(db *sql.DB, userID, messageID int) error {
_, err := db.Exec(`
UPDATE users_messages
SET is_archived = TRUE, archived_at = CURRENT_TIMESTAMP
WHERE id = ? AND recipientId = ?
`, messageID, userID)
return err
}
func MarkMessageAsRead(db *sql.DB, messageID, userID int) error {
result, err := db.Exec(`
UPDATE users_messages
SET is_read = TRUE
WHERE id = ? AND recipientId = ?
`, messageID, userID)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return fmt.Errorf("no matching message found for user_id=%d and message_id=%d", userID, messageID)
}
return nil
}
func RestoreMessage(db *sql.DB, userID, messageID int) error {
_, err := db.Exec(`
UPDATE users_messages
SET is_archived = FALSE, archived_at = NULL
WHERE id = ? AND recipientId = ?
`, messageID, userID)
return err
}

View File

@@ -0,0 +1,281 @@
-- 0001_initial.up.sql
-- Engine/charset notes:
-- - InnoDB for FK support
-- - utf8mb4 for full Unicode
-- Booleans are TINYINT(1). Dates use DATE/DATETIME/TIMESTAMP as appropriate.
-- USERS
CREATE TABLE IF NOT EXISTS users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(191) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
is_admin TINYINT(1) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- THUNDERBALL RESULTS
CREATE TABLE IF NOT EXISTS results_thunderball (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
draw_date DATE NOT NULL UNIQUE,
draw_id BIGINT UNSIGNED NOT NULL UNIQUE,
machine VARCHAR(50),
ballset VARCHAR(50),
ball1 TINYINT UNSIGNED,
ball2 TINYINT UNSIGNED,
ball3 TINYINT UNSIGNED,
ball4 TINYINT UNSIGNED,
ball5 TINYINT UNSIGNED,
thunderball TINYINT UNSIGNED
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- THUNDERBALL PRIZES
CREATE TABLE IF NOT EXISTS prizes_thunderball (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
draw_id BIGINT UNSIGNED NOT NULL,
draw_date DATE,
prize1 VARCHAR(50),
prize1_winners INT UNSIGNED,
prize1_per_winner INT UNSIGNED,
prize1_fund BIGINT UNSIGNED,
prize2 VARCHAR(50),
prize2_winners INT UNSIGNED,
prize2_per_winner INT UNSIGNED,
prize2_fund BIGINT UNSIGNED,
prize3 VARCHAR(50),
prize3_winners INT UNSIGNED,
prize3_per_winner INT UNSIGNED,
prize3_fund BIGINT UNSIGNED,
prize4 VARCHAR(50),
prize4_winners INT UNSIGNED,
prize4_per_winner INT UNSIGNED,
prize4_fund BIGINT UNSIGNED,
prize5 VARCHAR(50),
prize5_winners INT UNSIGNED,
prize5_per_winner INT UNSIGNED,
prize5_fund BIGINT UNSIGNED,
prize6 VARCHAR(50),
prize6_winners INT UNSIGNED,
prize6_per_winner INT UNSIGNED,
prize6_fund BIGINT UNSIGNED,
prize7 VARCHAR(50),
prize7_winners INT UNSIGNED,
prize7_per_winner INT UNSIGNED,
prize7_fund BIGINT UNSIGNED,
prize8 VARCHAR(50),
prize8_winners INT UNSIGNED,
prize8_per_winner INT UNSIGNED,
prize8_fund BIGINT UNSIGNED,
prize9 VARCHAR(50),
prize9_winners INT UNSIGNED,
prize9_per_winner INT UNSIGNED,
prize9_fund BIGINT UNSIGNED,
total_winners INT UNSIGNED,
total_prize_fund BIGINT UNSIGNED,
CONSTRAINT fk_prizes_tb_drawdate
FOREIGN KEY (draw_date) REFERENCES results_thunderball(draw_date)
ON UPDATE CASCADE ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- LOTTO RESULTS
CREATE TABLE IF NOT EXISTS results_lotto (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
draw_date DATE NOT NULL UNIQUE,
draw_id BIGINT UNSIGNED NOT NULL UNIQUE,
machine VARCHAR(50),
ballset VARCHAR(50),
ball1 TINYINT UNSIGNED,
ball2 TINYINT UNSIGNED,
ball3 TINYINT UNSIGNED,
ball4 TINYINT UNSIGNED,
ball5 TINYINT UNSIGNED,
ball6 TINYINT UNSIGNED,
bonusball TINYINT UNSIGNED
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- MY TICKETS
CREATE TABLE IF NOT EXISTS my_tickets (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
userId BIGINT UNSIGNED NOT NULL,
game_type VARCHAR(32) NOT NULL,
draw_date DATE NOT NULL,
ball1 TINYINT UNSIGNED,
ball2 TINYINT UNSIGNED,
ball3 TINYINT UNSIGNED,
ball4 TINYINT UNSIGNED,
ball5 TINYINT UNSIGNED,
ball6 TINYINT UNSIGNED,
bonus1 TINYINT UNSIGNED,
bonus2 TINYINT UNSIGNED,
duplicate TINYINT(1) NOT NULL DEFAULT 0,
purchase_date DATETIME,
purchase_method VARCHAR(50),
image_path VARCHAR(255),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
matched_main TINYINT UNSIGNED,
matched_bonus TINYINT UNSIGNED,
prize_tier VARCHAR(50),
is_winner TINYINT(1) NOT NULL DEFAULT 0,
prize_amount BIGINT,
prize_label VARCHAR(100),
syndicate_id BIGINT UNSIGNED,
CONSTRAINT fk_my_tickets_user
FOREIGN KEY (userId) REFERENCES users(id)
ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- USERS MESSAGES
CREATE TABLE IF NOT EXISTS users_messages (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
senderId BIGINT UNSIGNED NOT NULL,
recipientId BIGINT UNSIGNED NOT NULL,
subject VARCHAR(255) NOT NULL,
message MEDIUMTEXT,
is_read TINYINT(1) NOT NULL DEFAULT 0,
is_archived TINYINT(1) NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
archived_at DATETIME NULL,
CONSTRAINT fk_users_messages_sender
FOREIGN KEY (senderId) REFERENCES users(id)
ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT fk_users_messages_recipient
FOREIGN KEY (recipientId) REFERENCES users(id)
ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- USERS NOTIFICATIONS
CREATE TABLE IF NOT EXISTS users_notification (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
subject VARCHAR(255),
body MEDIUMTEXT,
is_read TINYINT(1) NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_users_notification_user
FOREIGN KEY (user_id) REFERENCES users(id)
ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- AUDITLOG
CREATE TABLE IF NOT EXISTS auditlog (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(191),
success TINYINT(1),
timestamp DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- LOG: TICKET MATCHING
CREATE TABLE IF NOT EXISTS log_ticket_matching (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
triggered_by VARCHAR(191),
run_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
tickets_matched INT,
winners_found INT,
notes TEXT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ADMIN ACCESS LOG
CREATE TABLE IF NOT EXISTS admin_access_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED,
accessed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
path VARCHAR(255),
ip VARCHAR(64),
user_agent VARCHAR(255),
CONSTRAINT fk_admin_access_user
FOREIGN KEY (user_id) REFERENCES users(id)
ON UPDATE CASCADE ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- AUDIT LOG (new)
CREATE TABLE IF NOT EXISTS audit_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED,
username VARCHAR(191),
action VARCHAR(191),
path VARCHAR(255),
ip VARCHAR(64),
user_agent VARCHAR(255),
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_audit_log_user
FOREIGN KEY (user_id) REFERENCES users(id)
ON UPDATE CASCADE ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- AUDIT LOGIN (new)
CREATE TABLE IF NOT EXISTS audit_login (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(191),
success TINYINT(1),
ip VARCHAR(64),
user_agent VARCHAR(255),
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- SYNDICATES
CREATE TABLE IF NOT EXISTS syndicates (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(191) NOT NULL,
description TEXT,
owner_id BIGINT UNSIGNED NOT NULL,
join_code VARCHAR(191) UNIQUE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_syndicates_owner
FOREIGN KEY (owner_id) REFERENCES users(id)
ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- SYNDICATE MEMBERS
CREATE TABLE IF NOT EXISTS syndicate_members (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
syndicate_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
role VARCHAR(32) NOT NULL DEFAULT 'member',
status VARCHAR(32) NOT NULL DEFAULT 'active',
joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_synmem_syn
FOREIGN KEY (syndicate_id) REFERENCES syndicates(id)
ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT fk_synmem_user
FOREIGN KEY (user_id) REFERENCES users(id)
ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT uq_synmem UNIQUE (syndicate_id, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- SYNDICATE INVITES
CREATE TABLE IF NOT EXISTS syndicate_invites (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
syndicate_id BIGINT UNSIGNED NOT NULL,
invited_user_id BIGINT UNSIGNED NOT NULL,
sent_by_user_id BIGINT UNSIGNED NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_syninv_syn
FOREIGN KEY (syndicate_id) REFERENCES syndicates(id)
ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT fk_syninv_invited
FOREIGN KEY (invited_user_id) REFERENCES users(id)
ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT fk_syninv_sender
FOREIGN KEY (sent_by_user_id) REFERENCES users(id)
ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- SYNDICATE INVITE TOKENS
CREATE TABLE IF NOT EXISTS syndicate_invite_tokens (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
syndicate_id BIGINT UNSIGNED NOT NULL,
token VARCHAR(191) NOT NULL UNIQUE,
invited_by_user_id BIGINT UNSIGNED NOT NULL,
accepted_by_user_id BIGINT UNSIGNED,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
accepted_at DATETIME,
expires_at DATETIME,
CONSTRAINT fk_syninvtoken_syn
FOREIGN KEY (syndicate_id) REFERENCES syndicates(id)
ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT fk_syninvtoken_invitedby
FOREIGN KEY (invited_by_user_id) REFERENCES users(id)
ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT fk_syninvtoken_acceptedby
FOREIGN KEY (accepted_by_user_id) REFERENCES users(id)
ON UPDATE CASCADE ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@@ -0,0 +1,3 @@
package storage
// ToDo: somethign must create notifications?

View File

@@ -0,0 +1,3 @@
package storage
// ToDo: not used, check messages and do something similar maybe dont store them?

View File

@@ -1,13 +1,28 @@
package storage package storage
//ToDo: should be using my own logging wrapper?
import ( import (
"database/sql" "database/sql"
"fmt"
"log" "log"
"synlotto-website/models" "synlotto-website/internal/models"
) )
func GetNotificationByID(db *sql.DB, userID, notificationID int) (*models.Notification, error) {
row := db.QueryRow(`
SELECT id, user_id, subject, body, is_read
FROM users_notification
WHERE id = ? AND user_id = ?
`, notificationID, userID)
var n models.Notification
err := row.Scan(&n.ID, &n.UserId, &n.Subject, &n.Body, &n.IsRead)
if err != nil {
return nil, err
}
return &n, nil
}
func GetNotificationCount(db *sql.DB, userID int) int { func GetNotificationCount(db *sql.DB, userID int) int {
var count int var count int
err := db.QueryRow(` err := db.QueryRow(`
@@ -46,40 +61,3 @@ func GetRecentNotifications(db *sql.DB, userID int, limit int) []models.Notifica
return notifications return notifications
} }
func MarkNotificationAsRead(db *sql.DB, userID int, notificationID int) error {
result, err := db.Exec(`
UPDATE users_notification
SET is_read = TRUE
WHERE id = ? AND user_id = ?
`, notificationID, userID)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return fmt.Errorf("no matching notification for user_id=%d and id=%d", userID, notificationID)
}
return nil
}
func GetNotificationByID(db *sql.DB, userID, notificationID int) (*models.Notification, error) {
row := db.QueryRow(`
SELECT id, user_id, subject, body, is_read
FROM users_notification
WHERE id = ? AND user_id = ?
`, notificationID, userID)
var n models.Notification
err := row.Scan(&n.ID, &n.UserId, &n.Subject, &n.Body, &n.IsRead)
if err != nil {
return nil, err
}
return &n, nil
}

View File

@@ -0,0 +1,29 @@
package storage
// ToDo: Should be logging fmt to my loggign wrapper
import (
"database/sql"
"fmt"
)
func MarkNotificationAsRead(db *sql.DB, userID int, notificationID int) error {
result, err := db.Exec(`
UPDATE users_notification
SET is_read = TRUE
WHERE id = ? AND user_id = ?
`, notificationID, userID)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return fmt.Errorf("no matching notification for user_id=%d and id=%d", userID, notificationID)
}
return nil
}

View File

@@ -0,0 +1,30 @@
package storage
import (
"database/sql"
"log"
"strings"
"synlotto-website/internal/models"
)
func InsertThunderballResult(db *sql.DB, res models.ThunderballResult) error {
stmt := `
INSERT INTO results_thunderball (
draw_date, machine, ballset,
ball1, ball2, ball3, ball4, ball5, thunderball
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);`
_, err := db.Exec(stmt,
res.DrawDate, res.Machine, res.BallSet,
res.Ball1, res.Ball2, res.Ball3, res.Ball4, res.Ball5, res.Thunderball,
)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
log.Printf("⚠️ Draw for %s already exists. Skipping insert.\n", res.DrawDate)
return nil
}
log.Println("❌ InsertThunderballResult error:", err)
}
return err
}

View File

@@ -12,6 +12,7 @@ const SchemaThunderballResults = `
CREATE TABLE IF NOT EXISTS results_thunderball ( CREATE TABLE IF NOT EXISTS results_thunderball (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
draw_date TEXT NOT NULL UNIQUE, draw_date TEXT NOT NULL UNIQUE,
draw_id INTEGER NOT NULL UNIQUE,
machine TEXT, machine TEXT,
ballset TEXT, ballset TEXT,
ball1 INTEGER, ball1 INTEGER,
@@ -72,6 +73,7 @@ const SchemaLottoResults = `
CREATE TABLE IF NOT EXISTS results_lotto ( CREATE TABLE IF NOT EXISTS results_lotto (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
draw_date TEXT NOT NULL UNIQUE, draw_date TEXT NOT NULL UNIQUE,
draw_id INTEGER NOT NULL UNIQUE,
machine TEXT, machine TEXT,
ballset TEXT, ballset TEXT,
ball1 INTEGER, ball1 INTEGER,

View File

@@ -0,0 +1,31 @@
package storage
import (
"database/sql"
"os"
)
// this is an example currenlty not true data
func Seed(db *sql.DB) error {
if _, err := db.Exec(`
INSERT INTO settings (k, v)
SELECT 'site_name', 'SynLotto'
WHERE NOT EXISTS (SELECT 1 FROM settings WHERE k='site_name');
`); err != nil {
// settings table is optional; remove if you don't use it
}
// 2) admin user (idempotent + secret from env)
adminUser := "admin"
adminHash := os.Getenv("ADMIN_BCRYPT_HASH") // or build from ADMIN_PASSWORD
if adminHash == "" {
// skip silently if you dont want to hard-require it
return nil
}
_, err := db.Exec(`
INSERT INTO users (username, password_hash, is_admin)
VALUES (?, ?, 1)
ON DUPLICATE KEY UPDATE is_admin=VALUES(is_admin)
`, adminUser, adminHash)
return err
}

View File

@@ -4,9 +4,10 @@ import (
"database/sql" "database/sql"
"log" "log"
"synlotto-website/config" "synlotto-website/internal/logging"
"synlotto-website/logging" "synlotto-website/internal/platform/config"
// ToDo: remove sqlite
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )

View File

@@ -0,0 +1,267 @@
package storage
// ToDo: See these are queries, alot of the others are functions with queries in, remove the functions and lea\ve the sql?...
// ToDo: The last seen statistic is done in days, maybe change or add in how many draws x days ways for ease.
// Top 5 main numbers since inception of the game.
const top5AllTime = `
SELECT ball AS Number, COUNT(*) AS Frequency
FROM (
SELECT ball1 AS ball FROM results_thunderball
UNION ALL SELECT ball2 FROM results_thunderball
UNION ALL SELECT ball3 FROM results_thunderball
UNION ALL SELECT ball4 FROM results_thunderball
UNION ALL SELECT ball5 FROM results_thunderball
)
GROUP BY ball
ORDER BY Frequency DESC, Number
LIMIT 5;`
// Top 5 main numbers since the ball count change on May 9th 2010.
const top5Since = `
SELECT ball AS Number, COUNT(*) AS Frequency
FROM (
SELECT ball1 AS ball FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT ball2 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT ball3 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT ball4 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT ball5 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
)
GROUP BY ball
ORDER BY Frequency DESC, Number
LIMIT 5;`
// Top 5 main numbers in the last 180 draws.
const top5Last180draws = `
SELECT ball AS Number, COUNT(*) AS Frequency
FROM (
SELECT ball1 AS ball FROM (
SELECT * FROM results_thunderball ORDER BY date(draw_date) DESC LIMIT 180
)
UNION ALL
SELECT ball2 FROM (
SELECT * FROM results_thunderball ORDER BY date(draw_date) DESC LIMIT 180
)
UNION ALL
SELECT ball3 FROM (
SELECT * FROM results_thunderball ORDER BY date(draw_date) DESC LIMIT 180
)
UNION ALL
SELECT ball4 FROM (
SELECT * FROM results_thunderball ORDER BY date(draw_date) DESC LIMIT 180
)
UNION ALL
SELECT ball5 FROM (
SELECT * FROM results_thunderball ORDER BY date(draw_date) DESC LIMIT 180
)
)
GROUP BY ball
ORDER BY Frequency DESC
LIMIT 5;`
// The top 5 thunderballs drawn since the inception of the game.
const top5ThunderballAllTime = `
SELECT thunderball AS Number, COUNT(*) AS Frequency
FROM (
SELECT thunderball AS thunderball FROM results_thunderball
)
GROUP BY thunderball
ORDER BY Frequency DESC, Number
LIMIT 5;`
// The top 5 thunderballs drawn since the ball count change on May 9th 2010.
const top5ThunderballSince = `
SELECT thunderball AS Number, COUNT(*) AS Frequency
FROM (
SELECT thunderball AS thunderball FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
)
GROUP BY thunderball
ORDER BY Frequency DESC, Number
LIMIT 5;`
const top5TunderballLast180draws = `
SELECT thunderball AS Number, COUNT(*) AS Frequency
FROM (
SELECT thunderball AS thunderball FROM (
SELECT * FROM results_thunderball ORDER BY date(draw_date) DESC LIMIT 180
)
)
GROUP BY thunderball
ORDER BY Frequency DESC
LIMIT 5;`
const thunderballMainLastSeen = `
SELECT
n.ball AS Number,
julianday('now') - julianday(MAX(r.draw_date)) AS DaysSinceLastDrawn,
MAX(r.draw_date) AS LastDrawDate
FROM (
SELECT ball1 AS ball, draw_date FROM results_thunderball
UNION ALL
SELECT ball2, draw_date FROM results_thunderball
UNION ALL
SELECT ball3, draw_date FROM results_thunderball
UNION ALL
SELECT ball4, draw_date FROM results_thunderball
UNION ALL
SELECT ball5, draw_date FROM results_thunderball
) AS r
JOIN (
-- This generates a list of all possible ball numbers (139)
SELECT 1 AS ball UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL
SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL
SELECT 11 UNION ALL SELECT 12 UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL SELECT 15 UNION ALL
SELECT 16 UNION ALL SELECT 17 UNION ALL SELECT 18 UNION ALL SELECT 19 UNION ALL SELECT 20 UNION ALL
SELECT 21 UNION ALL SELECT 22 UNION ALL SELECT 23 UNION ALL SELECT 24 UNION ALL SELECT 25 UNION ALL
SELECT 26 UNION ALL SELECT 27 UNION ALL SELECT 28 UNION ALL SELECT 29 UNION ALL SELECT 30 UNION ALL
SELECT 31 UNION ALL SELECT 32 UNION ALL SELECT 33 UNION ALL SELECT 34 UNION ALL SELECT 35 UNION ALL
SELECT 36 UNION ALL SELECT 37 UNION ALL SELECT 38 UNION ALL SELECT 39
) AS n ON n.ball = r.ball
GROUP BY n.ball
ORDER BY DaysSinceLastDrawn DESC;`
const thunderballLastSeen = `
SELECT
n.thunderball AS Number,
julianday('now') - julianday(MAX(r.draw_date)) AS DaysSinceLastDrawn,
MAX(r.draw_date) AS LastDrawDate
FROM (
SELECT thunderball AS thunderball, draw_date FROM results_thunderball
) AS r
JOIN (
SELECT 1 AS thunderball UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL
SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL
SELECT 11 UNION ALL SELECT 12 UNION ALL SELECT 13 UNION ALL SELECT 14
) AS n ON n.thunderball = r.thunderball
GROUP BY n.thunderball
ORDER BY DaysSinceLastDrawn DESC;`
const thunderballCommonPairsSince = `
WITH unpivot AS (
SELECT draw_date, ball1 AS ball FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball2 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball3 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball4 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball5 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
),
pairs AS (
SELECT a.draw_date,
MIN(a.ball, b.ball) AS ball_a,
MAX(a.ball, b.ball) AS ball_b
FROM unpivot a
JOIN unpivot b
ON a.draw_date = b.draw_date
AND a.ball < b.ball
)
SELECT ball_a, ball_b, COUNT(*) AS frequency
FROM pairs
GROUP BY ball_a, ball_b
ORDER BY frequency DESC, ball_a, ball_b
LIMIT 25;`
const thunderballCommonPairsLast180 = `
WITH recent AS (
SELECT * FROM results_thunderball
ORDER BY date(draw_date) DESC
LIMIT 180
),
unpivot AS (
SELECT draw_date, ball1 AS ball FROM recent
UNION ALL SELECT draw_date, ball2 FROM recent
UNION ALL SELECT draw_date, ball3 FROM recent
UNION ALL SELECT draw_date, ball4 FROM recent
UNION ALL SELECT draw_date, ball5 FROM recent
),
pairs AS (
SELECT a.draw_date,
MIN(a.ball, b.ball) AS ball_a,
MAX(a.ball, b.ball) AS ball_b
FROM unpivot a
JOIN unpivot b
ON a.draw_date = b.draw_date
AND a.ball < b.ball
)
SELECT ball_a, ball_b, COUNT(*) AS frequency
FROM pairs
GROUP BY ball_a, ball_b
ORDER BY frequency DESC, ball_a, ball_b
LIMIT 25;`
// Best pair balls if you choose x try picking these numbers that are frequencly seen with it (ToDo: Update this description)
// ToDo No All Time for this, go back and ensure everything has an all time for completeness.
const thunderballSepecificCommonPairsSince = `
WITH unpivot AS (
SELECT draw_date, ball1 AS ball FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball2 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball3 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball4 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball5 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
),
pairs AS (
SELECT a.draw_date,
MIN(a.ball, b.ball) AS ball_a,
MAX(a.ball, b.ball) AS ball_b
FROM unpivot a
JOIN unpivot b
ON a.draw_date = b.draw_date
AND a.ball < b.ball
)
SELECT
CASE WHEN ball_a = 26 THEN ball_b ELSE ball_a END AS partner,
COUNT(*) AS frequency
FROM pairs
WHERE ball_a = 26 OR ball_b = 26
GROUP BY partner
ORDER BY frequency DESC, partner
LIMIT 20;`
const thunderballCommonConsecutiveNumbersAllTime = `
WITH unpivot AS (
SELECT draw_date, ball1 AS ball FROM results_thunderball
UNION ALL SELECT draw_date, ball2 FROM results_thunderball
UNION ALL SELECT draw_date, ball3 FROM results_thunderball
UNION ALL SELECT draw_date, ball4 FROM results_thunderball
UNION ALL SELECT draw_date, ball5 FROM results_thunderball
),
pairs AS (
SELECT a.draw_date,
MIN(a.ball, b.ball) AS a_ball,
MAX(a.ball, b.ball) AS b_ball
FROM unpivot a
JOIN unpivot b
ON a.draw_date = b.draw_date
AND a.ball < b.ball
AND ABS(a.ball - b.ball) = 1 -- consecutive only
)
SELECT a_ball AS num1, b_ball AS num2, COUNT(*) AS frequency
FROM pairs
GROUP BY a_ball, b_ball
ORDER BY frequency DESC, num1, num2
LIMIT 25;
`
const thunderballCommonConsecutiveNumbersSince = `
WITH unpivot AS (
SELECT draw_date, ball1 AS ball FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball2 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball3 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball4 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball5 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
),
pairs AS (
SELECT a.draw_date,
MIN(a.ball, b.ball) AS a_ball,
MAX(a.ball, b.ball) AS b_ball
FROM unpivot a
JOIN unpivot b
ON a.draw_date = b.draw_date
AND a.ball < b.ball
AND ABS(a.ball - b.ball) = 1 -- consecutive only
)
SELECT a_ball AS num1, b_ball AS num2, COUNT(*) AS frequency
FROM pairs
GROUP BY a_ball, b_ball
ORDER BY frequency DESC, num1, num2
LIMIT 25;
`
// Wait, double check common number queries, consecutive and consecutive numbers make sure ive not mixed them up

View File

@@ -0,0 +1,21 @@
package storage
import (
"database/sql"
)
func AddMemberToSyndicate(db *sql.DB, syndicateID, userID int) error {
_, err := db.Exec(`
INSERT INTO syndicate_members (syndicate_id, user_id, joined_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
`, syndicateID, userID)
return err
}
func InviteUserToSyndicate(db *sql.DB, syndicateID, invitedUserID, senderID int) error {
_, err := db.Exec(`
INSERT INTO syndicate_invites (syndicate_id, invited_user_id, sent_by_user_id)
VALUES (?, ?, ?)
`, syndicateID, invitedUserID, senderID)
return err
}

View File

@@ -0,0 +1,146 @@
package storage
import (
"database/sql"
"synlotto-website/internal/models"
)
func GetInviteTokensForSyndicate(db *sql.DB, syndicateID int) []models.SyndicateInviteToken {
rows, err := db.Query(`
SELECT token, invited_by_user_id, accepted_by_user_id, created_at, expires_at, accepted_at
FROM syndicate_invite_tokens
WHERE syndicate_id = ?
ORDER BY created_at DESC
`, syndicateID)
if err != nil {
return nil
}
defer rows.Close()
var tokens []models.SyndicateInviteToken
for rows.Next() {
var t models.SyndicateInviteToken
_ = rows.Scan(
&t.Token,
&t.InvitedByUserID,
&t.AcceptedByUserID,
&t.CreatedAt,
&t.ExpiresAt,
&t.AcceptedAt,
)
tokens = append(tokens, t)
}
return tokens
}
func GetPendingSyndicateInvites(db *sql.DB, userID int) []models.SyndicateInvite {
rows, err := db.Query(`
SELECT id, syndicate_id, invited_user_id, sent_by_user_id, status, created_at
FROM syndicate_invites
WHERE invited_user_id = ? AND status = 'pending'
`, userID)
if err != nil {
return nil
}
defer rows.Close()
var invites []models.SyndicateInvite
for rows.Next() {
var i models.SyndicateInvite
rows.Scan(&i.ID, &i.SyndicateID, &i.InvitedUserID, &i.SentByUserID, &i.Status, &i.CreatedAt)
invites = append(invites, i)
}
return invites
}
func GetSyndicateByID(db *sql.DB, id int) (*models.Syndicate, error) {
row := db.QueryRow(`SELECT id, name, description, owner_id, created_at FROM syndicates WHERE id = ?`, id)
var s models.Syndicate
err := row.Scan(&s.ID, &s.Name, &s.Description, &s.OwnerID, &s.CreatedAt)
if err != nil {
return nil, err
}
return &s, nil
}
func GetSyndicatesByMember(db *sql.DB, userID int) []models.Syndicate {
rows, err := db.Query(`
SELECT s.id, s.name, s.description, s.created_at, s.owner_id
FROM syndicates s
JOIN syndicate_members m ON s.id = m.syndicate_id
WHERE m.user_id = ?`, userID)
if err != nil {
return nil
}
defer rows.Close()
var syndicates []models.Syndicate
for rows.Next() {
var s models.Syndicate
err := rows.Scan(&s.ID, &s.Name, &s.Description, &s.CreatedAt, &s.OwnerID)
if err == nil {
syndicates = append(syndicates, s)
}
}
return syndicates
}
func GetSyndicatesByOwner(db *sql.DB, ownerID int) []models.Syndicate {
rows, err := db.Query(`
SELECT id, name, description, created_at, owner_id
FROM syndicates
WHERE owner_id = ?`, ownerID)
if err != nil {
return nil
}
defer rows.Close()
var syndicates []models.Syndicate
for rows.Next() {
var s models.Syndicate
err := rows.Scan(&s.ID, &s.Name, &s.Description, &s.CreatedAt, &s.OwnerID)
if err == nil {
syndicates = append(syndicates, s)
}
}
return syndicates
}
func GetSyndicateMembers(db *sql.DB, syndicateID int) []models.SyndicateMember {
rows, err := db.Query(`
SELECT m.user_id, u.username, m.joined_at
FROM syndicate_members m
JOIN users u ON u.id = m.user_id
WHERE m.syndicate_id = ?
`, syndicateID)
if err != nil {
return nil
}
defer rows.Close()
var members []models.SyndicateMember
for rows.Next() {
var m models.SyndicateMember
err := rows.Scan(&m.UserID, &m.UserID, &m.JoinedAt)
if err == nil {
members = append(members, m)
}
}
return members
}
func IsSyndicateManager(db *sql.DB, syndicateID, userID int) bool {
var count int
err := db.QueryRow(`
SELECT COUNT(*) FROM syndicates
WHERE id = ? AND owner_id = ?
`, syndicateID, userID).Scan(&count)
return err == nil && count > 0
}
func IsSyndicateMember(db *sql.DB, syndicateID, userID int) bool {
var count int
err := db.QueryRow(`SELECT COUNT(*) FROM syndicate_members WHERE syndicate_id = ? AND user_id = ?`, syndicateID, userID).Scan(&count)
return err == nil && count > 0
}

View File

@@ -0,0 +1,125 @@
package storage
import (
"database/sql"
"fmt"
"synlotto-website/internal/models"
"time"
)
// todo should be a ticket function?
func GetSyndicateTickets(db *sql.DB, syndicateID int) []models.Ticket {
rows, err := db.Query(`
SELECT id, userId, syndicateId, game_type, draw_date, ball1, ball2, ball3, ball4, ball5, ball6,
bonus1, bonus2, matched_main, matched_bonus, prize_tier, prize_amount, prize_label, is_winner
FROM my_tickets
WHERE syndicateId = ?
ORDER BY draw_date DESC
`, syndicateID)
if err != nil {
return nil
}
defer rows.Close()
var tickets []models.Ticket
for rows.Next() {
var t models.Ticket
err := rows.Scan(
&t.Id, &t.UserId, &t.SyndicateId, &t.GameType, &t.DrawDate,
&t.Ball1, &t.Ball2, &t.Ball3, &t.Ball4, &t.Ball5, &t.Ball6,
&t.Bonus1, &t.Bonus2, &t.MatchedMain, &t.MatchedBonus,
&t.PrizeTier, &t.PrizeAmount, &t.PrizeLabel, &t.IsWinner,
)
if err == nil {
tickets = append(tickets, t)
}
}
return tickets
}
// both a read and inset break up
func AcceptInvite(db *sql.DB, inviteID, userID int) error {
var syndicateID int
err := db.QueryRow(`
SELECT syndicate_id FROM syndicate_invites
WHERE id = ? AND invited_user_id = ? AND status = 'pending'
`, inviteID, userID).Scan(&syndicateID)
if err != nil {
return err
}
if err := UpdateInviteStatus(db, inviteID, "accepted"); err != nil {
return err
}
_, err = db.Exec(`
INSERT INTO syndicate_members (syndicate_id, user_id, joined_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
`, syndicateID, userID)
return err
}
func CreateSyndicate(db *sql.DB, ownerID int, name, description string) (int64, error) {
tx, err := db.Begin()
if err != nil {
return 0, err
}
defer tx.Rollback()
result, err := tx.Exec(`
INSERT INTO syndicates (name, description, owner_id, created_at)
VALUES (?, ?, ?, ?)
`, name, description, ownerID, time.Now())
if err != nil {
return 0, fmt.Errorf("failed to create syndicate: %w", err)
}
syndicateID, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("failed to get syndicate ID: %w", err)
}
_, err = tx.Exec(`
INSERT INTO syndicate_members (syndicate_id, user_id, role, joined_at)
VALUES (?, ?, 'manager', CURRENT_TIMESTAMP)
`, syndicateID, ownerID)
if err != nil {
return 0, fmt.Errorf("failed to add owner as member: %w", err)
}
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("commit failed: %w", err)
}
return syndicateID, nil
}
func InviteToSyndicate(db *sql.DB, inviterID, syndicateID int, username string) error {
var inviteeID int
err := db.QueryRow(`
SELECT id FROM users WHERE username = ?
`, username).Scan(&inviteeID)
if err == sql.ErrNoRows {
return fmt.Errorf("user not found")
} else if err != nil {
return err
}
var count int
err = db.QueryRow(`
SELECT COUNT(*) FROM syndicate_members
WHERE syndicate_id = ? AND user_id = ?
`, syndicateID, inviteeID).Scan(&count)
if err != nil {
return err
}
if count > 0 {
return fmt.Errorf("user already a member or invited")
}
_, err = db.Exec(`
INSERT INTO syndicate_members (syndicate_id, user_id, is_manager, status)
VALUES (?, ?, 0, 'invited')
`, syndicateID, inviteeID)
return err
}

View File

@@ -0,0 +1,14 @@
package storage
import (
"database/sql"
)
func UpdateInviteStatus(db *sql.DB, inviteID int, status string) error {
_, err := db.Exec(`
UPDATE syndicate_invites
SET status = ?
WHERE id = ?
`, status, inviteID)
return err
}

View File

@@ -3,32 +3,11 @@ package storage
import ( import (
"database/sql" "database/sql"
"log" "log"
"strings" "synlotto-website/internal/helpers"
"synlotto-website/helpers" "synlotto-website/internal/models"
"synlotto-website/models"
) )
func InsertThunderballResult(db *sql.DB, res models.ThunderballResult) error { // ToDo: Has both insert and select need to break into read and write.
stmt := `
INSERT INTO results_thunderball (
draw_date, machine, ballset,
ball1, ball2, ball3, ball4, ball5, thunderball
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);`
_, err := db.Exec(stmt,
res.DrawDate, res.Machine, res.BallSet,
res.Ball1, res.Ball2, res.Ball3, res.Ball4, res.Ball5, res.Thunderball,
)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
log.Printf("⚠️ Draw for %s already exists. Skipping insert.\n", res.DrawDate)
return nil
}
log.Println("❌ InsertThunderballResult error:", err)
}
return err
}
func InsertTicket(db *sql.DB, ticket models.Ticket) error { func InsertTicket(db *sql.DB, ticket models.Ticket) error {
var bonus1Val interface{} var bonus1Val interface{}
var bonus2Val interface{} var bonus2Val interface{}

View File

@@ -0,0 +1,22 @@
package storage
// ToDo.. "errors" should this not be using my custom log wrapper
import (
"context"
"database/sql"
"errors"
"synlotto-website/internal/models"
)
type UsersRepo struct{ db *sql.DB}
func NewUsersRepo(db *.sql.DB) *UsersRepo { return &UsersRepo{db: db} }
func (r *UsersRepo) Create(ctx context.Context, username, passwordHash string, isAdmin bool) error {
_, err := r.db.ExecContext(ctx,
`INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, ?)`,
username, passwordHash, isAdmin,
)
return err
}

View File

@@ -3,8 +3,8 @@ package storage
import ( import (
"database/sql" "database/sql"
"synlotto-website/logging" "synlotto-website/internal/logging"
"synlotto-website/models" "synlotto-website/internal/models"
) )
func GetUserByID(db *sql.DB, id int) *models.User { func GetUserByID(db *sql.DB, id int) *models.User {

View File

@@ -27,14 +27,14 @@ func main() {
logging.LogConfig(appState.Config) logging.LogConfig(appState.Config)
db := storage.InitDB("synlotto.db") db := storage.InitDB("synlotto.db")
models.SetDB(db) // Should be in storage not models. models.SetDB(db) // ToDo: Should be in storage not models.
err = bootstrap.InitSession(appState.Config) err = bootstrap.InitSession(appState.Config)
if err != nil { if err != nil {
logging.Error("❌ Failed to init session: %v", err) logging.Error("❌ Failed to init session: %v", err)
} }
// if err := bootstrap.InitLicenseChecker(appState.Config); err != nil { // ToDo: if err := bootstrap.InitLicenseChecker(appState.Config); err != nil {
// logging.Error("❌ Invalid license: %v", err) // logging.Error("❌ Invalid license: %v", err)
// } // }
@@ -48,6 +48,7 @@ func main() {
routes.SetupAccountRoutes(mux, db) routes.SetupAccountRoutes(mux, db)
routes.SetupResultRoutes(mux, db) routes.SetupResultRoutes(mux, db)
routes.SetupSyndicateRoutes(mux, db) routes.SetupSyndicateRoutes(mux, db)
routes.SetupStatisticsRoutes(mux, db)
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
mux.HandleFunc("/", handlers.Home(db)) mux.HandleFunc("/", handlers.Home(db))

View File

@@ -1,61 +0,0 @@
package models
import (
"database/sql"
"log"
"time"
)
type User struct {
Id int
Username string
PasswordHash string
IsAdmin bool
}
type Notification struct {
ID int
UserId int
Subject string
Body string
IsRead bool
CreatedAt time.Time
}
type Message struct {
ID int
SenderId int
RecipientId int
Subject string
Message string
IsRead bool
CreatedAt time.Time
ArchivedAt *time.Time
}
var db *sql.DB
func SetDB(database *sql.DB) {
db = database
}
func CreateUser(username, passwordHash string) error {
_, err := db.Exec("INSERT INTO users (username, password_hash) VALUES (?, ?)", username, passwordHash)
//ToDo: Why is SQL in here?
return err
}
func GetUserByUsername(username string) *User {
row := db.QueryRow("SELECT id, username, password_hash FROM users WHERE username = ?", username)
var user User
err := row.Scan(&user.Id, &user.Username, &user.PasswordHash)
if err != nil {
if err != sql.ErrNoRows {
log.Println("DB error:", err)
}
return nil
}
return &user
}

View File

@@ -1,46 +0,0 @@
package rules
import "synlotto-website/models"
type PrizeInfo struct {
Tier string
Amount float64
Label string
}
var ThunderballPrizeRules = []models.PrizeRule{
{Game: "Thunderball", MainMatches: 0, BonusMatches: 1, Tier: "Tier 1"},
{Game: "Thunderball", MainMatches: 1, BonusMatches: 1, Tier: "Tier 2"},
{Game: "Thunderball", MainMatches: 2, BonusMatches: 1, Tier: "Tier 3"},
{Game: "Thunderball", MainMatches: 3, BonusMatches: 0, Tier: "Tier 4"},
{Game: "Thunderball", MainMatches: 3, BonusMatches: 1, Tier: "Tier 5"},
{Game: "Thunderball", MainMatches: 4, BonusMatches: 0, Tier: "Tier 6"},
{Game: "Thunderball", MainMatches: 4, BonusMatches: 1, Tier: "Tier 7"},
{Game: "Thunderball", MainMatches: 5, BonusMatches: 0, Tier: "Second"},
{Game: "Thunderball", MainMatches: 5, BonusMatches: 1, Tier: "Jackpot"},
}
func GetThunderballPrizeIndex(main, bonus int) (int, bool) {
switch {
case main == 0 && bonus == 1:
return 9, true
case main == 1 && bonus == 1:
return 8, true
case main == 2 && bonus == 1:
return 7, true
case main == 3 && bonus == 0:
return 6, true
case main == 3 && bonus == 1:
return 5, true
case main == 4 && bonus == 0:
return 4, true
case main == 4 && bonus == 1:
return 3, true
case main == 5 && bonus == 0:
return 2, true
case main == 5 && bonus == 1:
return 1, true
default:
return 0, false
}
}

View File

@@ -1,22 +0,0 @@
package storage
import (
"net/http"
"time"
"synlotto-website/logging"
)
func LogLoginAttempt(r *http.Request, username string, success bool) {
ip := r.RemoteAddr
userAgent := r.UserAgent()
_, err := db.Exec(
`INSERT INTO audit_login (username, success, ip, user_agent, timestamp)
VALUES (?, ?, ?, ?, ?)`,
username, success, ip, userAgent, time.Now().UTC(),
)
if err != nil {
logging.Info("❌ Failed to log login:", err)
}
}

Some files were not shown because too many files have changed in this diff Show More