Compare commits
9 Commits
e0a2e5430e
...
0f60be448d
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f60be448d | |||
| 82f457c5a4 | |||
| b098915ab9 | |||
| 21ebc9c34b | |||
| d53e27eea8 | |||
| 752db0b89d | |||
| 8d06a7a962 | |||
| 7597fff8b1 | |||
| 58dd313703 |
5
go.mod
5
go.mod
@@ -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
13
go.sum
@@ -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=
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
@@ -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 (
|
||||||
@@ -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 {
|
||||||
@@ -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 {
|
||||||
@@ -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 {
|
||||||
@@ -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
|
||||||
@@ -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 {
|
||||||
@@ -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 {
|
||||||
@@ -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 {
|
||||||
@@ -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 {
|
||||||
@@ -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"
|
||||||
)
|
)
|
||||||
@@ -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 {
|
||||||
@@ -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 {
|
||||||
@@ -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 {
|
||||||
@@ -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 {
|
||||||
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/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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
@@ -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 {
|
||||||
@@ -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"
|
||||||
)
|
)
|
||||||
@@ -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) {
|
||||||
@@ -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"
|
||||||
)
|
)
|
||||||
@@ -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) {
|
||||||
@@ -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 {
|
||||||
@@ -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 {
|
||||||
@@ -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 {
|
||||||
@@ -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) {
|
||||||
@@ -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) {
|
||||||
@@ -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) {
|
||||||
13
internal/http/routes/statisticroutes.go
Normal file
13
internal/http/routes/statisticroutes.go
Normal 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)))
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
@@ -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) {
|
||||||
@@ -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"`
|
||||||
9
internal/models/machine.go
Normal file
9
internal/models/machine.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type MachineUsage struct {
|
||||||
|
Machine string
|
||||||
|
DrawsUsed int
|
||||||
|
PctOfDraws float64
|
||||||
|
FirstUsed string
|
||||||
|
LastUsed string
|
||||||
|
}
|
||||||
15
internal/models/prediction.go
Normal file
15
internal/models/prediction.go
Normal 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
|
||||||
|
}
|
||||||
18
internal/models/statistics.go
Normal file
18
internal/models/statistics.go
Normal 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
34
internal/models/user.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"synlotto-website/models"
|
"synlotto-website/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppState struct {
|
type AppState struct {
|
||||||
@@ -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"
|
||||||
)
|
)
|
||||||
@@ -3,7 +3,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"synlotto-website/models"
|
"synlotto-website/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
43
internal/rules/thunderball/rules.go
Normal file
43
internal/rules/thunderball/rules.go
Normal 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
23
internal/rules/types.go
Normal 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"
|
||||||
|
)
|
||||||
@@ -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 {
|
||||||
@@ -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 {
|
||||||
@@ -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) {
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
78
internal/storage/mysql/db.go
Normal file
78
internal/storage/mysql/db.go
Normal 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
|
||||||
|
}
|
||||||
13
internal/storage/mysql/messages/create.go
Normal file
13
internal/storage/mysql/messages/create.go
Normal 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
|
||||||
|
}
|
||||||
3
internal/storage/mysql/messages/delete.go
Normal file
3
internal/storage/mysql/messages/delete.go
Normal 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
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
44
internal/storage/mysql/messages/update.go
Normal file
44
internal/storage/mysql/messages/update.go
Normal 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
|
||||||
|
}
|
||||||
281
internal/storage/mysql/migrations/0001_initial_create.up.sql
Normal file
281
internal/storage/mysql/migrations/0001_initial_create.up.sql
Normal 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;
|
||||||
3
internal/storage/mysql/notifications/create.go
Normal file
3
internal/storage/mysql/notifications/create.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
// ToDo: somethign must create notifications?
|
||||||
3
internal/storage/mysql/notifications/delete.go
Normal file
3
internal/storage/mysql/notifications/delete.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
// ToDo: not used, check messages and do something similar maybe dont store them?
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
29
internal/storage/mysql/notifications/update.go
Normal file
29
internal/storage/mysql/notifications/update.go
Normal 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
|
||||||
|
}
|
||||||
30
internal/storage/mysql/results/thunderball/create.go
Normal file
30
internal/storage/mysql/results/thunderball/create.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
31
internal/storage/mysql/seeds/thunderball_seed.go
Normal file
31
internal/storage/mysql/seeds/thunderball_seed.go
Normal 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 don’t 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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -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 (1–39)
|
||||||
|
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
|
||||||
21
internal/storage/mysql/syndicate/create.go
Normal file
21
internal/storage/mysql/syndicate/create.go
Normal 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
|
||||||
|
}
|
||||||
146
internal/storage/mysql/syndicate/read.go
Normal file
146
internal/storage/mysql/syndicate/read.go
Normal 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
|
||||||
|
}
|
||||||
125
internal/storage/mysql/syndicate/syndicate.go
Normal file
125
internal/storage/mysql/syndicate/syndicate.go
Normal 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
|
||||||
|
}
|
||||||
14
internal/storage/mysql/syndicate/update.go
Normal file
14
internal/storage/mysql/syndicate/update.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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{}
|
||||||
22
internal/storage/mysql/users/create.go
Normal file
22
internal/storage/mysql/users/create.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
5
main.go
5
main.go
@@ -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))
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
Reference in New Issue
Block a user