Compare commits
8 Commits
0f60be448d
...
4a6bfad880
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a6bfad880 | |||
| 04c3cb3851 | |||
| c911bf9151 | |||
| 86be6479f1 | |||
| 07117ba35e | |||
| ac1f6e9399 | |||
| fb07c4a5eb | |||
| 7276903733 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
|
main.exe
|
||||||
synlotto-website.exe
|
synlotto-website.exe
|
||||||
synlotto.db
|
synlotto.db
|
||||||
59
cmd/api/main.go
Normal file
59
cmd/api/main.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Path /cmd/api
|
||||||
|
// File: main.go
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
|
||||||
|
"synlotto-website/internal/http/middleware"
|
||||||
|
"synlotto-website/internal/http/routes"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app, err := bootstrap.Load("internal\\platform\\config\\config.json")
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("bootstrap: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
templateHelpers.InitSessionManager(app.SessionManager)
|
||||||
|
templateHelpers.InitSiteMeta(app.Config.Site.SiteName, app.Config.Site.CopyrightYearStart, 0)
|
||||||
|
|
||||||
|
// Global middleware that depends on *App
|
||||||
|
app.Router.Use(middleware.AuthMiddleware())
|
||||||
|
app.Router.Use(middleware.RememberMiddleware(app))
|
||||||
|
|
||||||
|
// Route registration lives OUTSIDE bootstrap
|
||||||
|
routes.RegisterHomeRoutes(app)
|
||||||
|
routes.RegisterAccountRoutes(app)
|
||||||
|
routes.RegisterAdminRoutes(app)
|
||||||
|
routes.RegisterSyndicateRoutes(app)
|
||||||
|
routes.RegisterStatisticsRoutes(app)
|
||||||
|
|
||||||
|
srv := app.Server
|
||||||
|
go func() {
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Printf("Server running on http://%s\n", srv.Addr)
|
||||||
|
|
||||||
|
stop := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||||
|
<-stop
|
||||||
|
|
||||||
|
fmt.Println("Shutting down...")
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
_ = srv.Shutdown(ctx)
|
||||||
|
}
|
||||||
52
go.mod
52
go.mod
@@ -3,28 +3,44 @@ module synlotto-website
|
|||||||
go 1.24.1
|
go 1.24.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gorilla/csrf v1.7.2
|
github.com/alexedwards/scs/v2 v2.9.0
|
||||||
github.com/gorilla/sessions v1.4.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
golang.org/x/crypto v0.36.0
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
|
github.com/justinas/nosurf v1.2.0
|
||||||
|
golang.org/x/crypto v0.40.0
|
||||||
golang.org/x/time v0.11.0
|
golang.org/x/time v0.11.0
|
||||||
modernc.org/sqlite v1.36.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/bytedance/sonic v1.14.0 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.0 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // 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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
modernc.org/libc v1.61.13 // indirect
|
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
modernc.org/memory v1.8.2 // indirect
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
|
go.uber.org/mock v0.5.0 // indirect
|
||||||
|
golang.org/x/arch v0.20.0 // indirect
|
||||||
|
golang.org/x/mod v0.25.0 // indirect
|
||||||
|
golang.org/x/net v0.42.0 // indirect
|
||||||
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
|
golang.org/x/text v0.27.0 // indirect
|
||||||
|
golang.org/x/tools v0.34.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
152
go.sum
152
go.sum
@@ -1,72 +1,98 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
|
||||||
|
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||||
|
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
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/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/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
github.com/justinas/nosurf v1.2.0 h1:yMs1bSRrNiwXk4AS6n8vL2Ssgpb9CB25T/4xrixaK0s=
|
||||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
github.com/justinas/nosurf v1.2.0/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ=
|
||||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||||
|
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
|
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||||
|
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
|
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||||
|
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||||
|
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||||
|
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
|
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
|
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||||
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||||
|
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
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.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||||
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
|
||||||
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
|
|
||||||
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
|
||||||
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
|
|
||||||
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
|
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
|
||||||
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
|
|
||||||
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
|
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
|
||||||
modernc.org/sqlite v1.36.1 h1:bDa8BJUH4lg6EGkLbahKe/8QqoF8p9gArSc6fTqYhyQ=
|
|
||||||
modernc.org/sqlite v1.36.1/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=
|
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
httpHelpers "synlotto-website/internal/helpers/http"
|
|
||||||
securityHelpers "synlotto-website/internal/helpers/security"
|
|
||||||
templateHelpers "synlotto-website/internal/helpers/template"
|
|
||||||
|
|
||||||
"synlotto-website/internal/logging"
|
|
||||||
"synlotto-website/internal/models"
|
|
||||||
"synlotto-website/internal/storage"
|
|
||||||
|
|
||||||
"github.com/gorilla/csrf"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Login(db *sql.DB) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method == http.MethodGet {
|
|
||||||
session, _ := httpHelpers.GetSession(w, r)
|
|
||||||
if _, ok := session.Values["user_id"].(int); ok {
|
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("login.html", "templates/account/login.html")
|
|
||||||
data := models.TemplateData{}
|
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
|
||||||
context["csrfField"] = csrf.TemplateField(r)
|
|
||||||
|
|
||||||
if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil {
|
|
||||||
logging.Info("❌ Template render error:", err)
|
|
||||||
http.Error(w, "Error rendering login page", http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
username := r.FormValue("username")
|
|
||||||
password := r.FormValue("password")
|
|
||||||
|
|
||||||
// ToDo: this outputs password in clear text remove or obscure!
|
|
||||||
logging.Info("🔐 Login attempt - Username: %s, Password: %s", username, password)
|
|
||||||
|
|
||||||
user := storage.GetUserByUsername(db, username)
|
|
||||||
if user == nil {
|
|
||||||
logging.Info("❌ User not found: %s", username)
|
|
||||||
storage.LogLoginAttempt(r, username, false)
|
|
||||||
|
|
||||||
session, _ := httpHelpers.GetSession(w, r)
|
|
||||||
session.Values["flash"] = "Invalid username or password."
|
|
||||||
session.Save(r, w)
|
|
||||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !securityHelpers.CheckPasswordHash(user.PasswordHash, password) {
|
|
||||||
logging.Info("❌ Password mismatch for user: %s", username)
|
|
||||||
storage.LogLoginAttempt(r, username, false)
|
|
||||||
|
|
||||||
session, _ := httpHelpers.GetSession(w, r)
|
|
||||||
session.Values["flash"] = "Invalid username or password."
|
|
||||||
session.Save(r, w)
|
|
||||||
log.Printf("login has did it")
|
|
||||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logging.Info("✅ Login successful for user: %s", username)
|
|
||||||
storage.LogLoginAttempt(r, username, true)
|
|
||||||
|
|
||||||
session, _ := httpHelpers.GetSession(w, r)
|
|
||||||
for k := range session.Values {
|
|
||||||
delete(session.Values, k)
|
|
||||||
}
|
|
||||||
|
|
||||||
session.Values["user_id"] = user.Id
|
|
||||||
session.Values["last_activity"] = time.Now().UTC()
|
|
||||||
|
|
||||||
if r.FormValue("remember") == "on" {
|
|
||||||
session.Options.MaxAge = 60 * 60 * 24 * 30
|
|
||||||
} else {
|
|
||||||
session.Options.MaxAge = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := session.Save(r, w); err != nil {
|
|
||||||
logging.Info("❌ Failed to save session: %v", err)
|
|
||||||
} else {
|
|
||||||
logging.Info("✅ Session saved for user: %s", username)
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Logout(w http.ResponseWriter, r *http.Request) {
|
|
||||||
session, _ := httpHelpers.GetSession(w, r)
|
|
||||||
|
|
||||||
for k := range session.Values {
|
|
||||||
delete(session.Values, k)
|
|
||||||
}
|
|
||||||
|
|
||||||
session.Values["flash"] = "You've been logged out."
|
|
||||||
session.Options.MaxAge = 5
|
|
||||||
|
|
||||||
err := session.Save(r, w)
|
|
||||||
if err != nil {
|
|
||||||
logging.Error("❌ Logout session save failed:", err)
|
|
||||||
}
|
|
||||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Signup(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method == http.MethodGet {
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("signup.html", "templates/account/signup.html")
|
|
||||||
|
|
||||||
tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{
|
|
||||||
"csrfField": csrf.TemplateField(r),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
username := r.FormValue("username")
|
|
||||||
password := r.FormValue("password")
|
|
||||||
|
|
||||||
hashed, err := securityHelpers.HashPassword(password)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Server error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = models.CreateUser(username, hashed)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Could not create user", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
|
||||||
}
|
|
||||||
81
internal/handlers/account/login.go
Normal file
81
internal/handlers/account/login.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package accountHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/justinas/nosurf"
|
||||||
|
|
||||||
|
securityHelpers "synlotto-website/internal/helpers/security"
|
||||||
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
"synlotto-website/internal/logging"
|
||||||
|
"synlotto-website/internal/models"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
auditlogStorage "synlotto-website/internal/storage/auditlog"
|
||||||
|
usersStorage "synlotto-website/internal/storage/users"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoginGet(c *gin.Context) {
|
||||||
|
app := c.MustGet("app").(*bootstrap.App)
|
||||||
|
sm := app.SessionManager
|
||||||
|
|
||||||
|
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{})
|
||||||
|
|
||||||
|
if f := sm.PopString(c.Request.Context(), "flash"); f != "" {
|
||||||
|
ctx["Flash"] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx["CSRFToken"] = nosurf.Token(c.Request)
|
||||||
|
|
||||||
|
tmpl := templateHelpers.LoadTemplateFiles("layout.html", "web/templates/account/login.html")
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil {
|
||||||
|
logging.Info("❌ Template render error: %v", err)
|
||||||
|
c.String(http.StatusInternalServerError, "Error rendering login page")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoginPost(c *gin.Context) {
|
||||||
|
app := c.MustGet("app").(*bootstrap.App)
|
||||||
|
sm := app.SessionManager
|
||||||
|
db := app.DB
|
||||||
|
|
||||||
|
r := c.Request
|
||||||
|
w := c.Writer
|
||||||
|
|
||||||
|
username := r.FormValue("username")
|
||||||
|
password := r.FormValue("password")
|
||||||
|
|
||||||
|
logging.Info("🔐 Login attempt - Username: %s", username)
|
||||||
|
|
||||||
|
user := usersStorage.GetUserByUsername(db, username)
|
||||||
|
if user == nil {
|
||||||
|
logging.Info("❌ User not found: %s", username)
|
||||||
|
auditlogStorage.LogLoginAttempt(db, r.RemoteAddr, r.UserAgent(), username, false)
|
||||||
|
sm.Put(r.Context(), "flash", "Invalid username or password.")
|
||||||
|
c.Redirect(http.StatusSeeOther, "/account/login")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !securityHelpers.CheckPasswordHash(user.PasswordHash, password) {
|
||||||
|
logging.Info("❌ Password mismatch for user: %s", username)
|
||||||
|
auditlogStorage.LogLoginAttempt(db, r.RemoteAddr, r.UserAgent(), username, false)
|
||||||
|
sm.Put(r.Context(), "flash", "Invalid username or password.")
|
||||||
|
c.Redirect(http.StatusSeeOther, "/account/login")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("✅ Login successful for user: %s", username)
|
||||||
|
auditlogStorage.LogLoginAttempt(db, r.RemoteAddr, r.UserAgent(), username, true)
|
||||||
|
|
||||||
|
_ = sm.Destroy(r.Context())
|
||||||
|
_ = sm.RenewToken(r.Context())
|
||||||
|
|
||||||
|
sm.Put(r.Context(), "user_id", user.Id)
|
||||||
|
sm.Put(r.Context(), "last_activity", time.Now().UTC())
|
||||||
|
sm.Put(r.Context(), "flash", "Welcome back, "+user.Username+"!")
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
|
}
|
||||||
19
internal/handlers/account/logout.go
Normal file
19
internal/handlers/account/logout.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package accountHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Logout(c *gin.Context) {
|
||||||
|
app := c.MustGet("app").(*bootstrap.App)
|
||||||
|
sm := app.SessionManager
|
||||||
|
|
||||||
|
_ = sm.Destroy(c.Request.Context())
|
||||||
|
_ = sm.RenewToken(c.Request.Context())
|
||||||
|
sm.Put(c.Request.Context(), "flash", "You've been logged out.")
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/account/login")
|
||||||
|
}
|
||||||
170
internal/handlers/account/signup.go
Normal file
170
internal/handlers/account/signup.go
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
// internal/handlers/account/signup.go
|
||||||
|
package accountHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
httphelpers "synlotto-website/internal/helpers/http"
|
||||||
|
securityHelpers "synlotto-website/internal/helpers/security"
|
||||||
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
auditlogStorage "synlotto-website/internal/storage/auditlog"
|
||||||
|
usersStorage "synlotto-website/internal/storage/users"
|
||||||
|
|
||||||
|
"synlotto-website/internal/logging"
|
||||||
|
"synlotto-website/internal/models"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/justinas/nosurf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// kept for handler-local parsing only (NOT stored in session)
|
||||||
|
type registerForm struct {
|
||||||
|
Username string
|
||||||
|
Email string
|
||||||
|
Password string
|
||||||
|
PasswordConfirm string
|
||||||
|
AcceptTerms bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func SignupGet(c *gin.Context) {
|
||||||
|
app := c.MustGet("app").(*bootstrap.App)
|
||||||
|
sm := app.SessionManager
|
||||||
|
|
||||||
|
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{})
|
||||||
|
if f := sm.PopString(c.Request.Context(), "flash"); f != "" {
|
||||||
|
ctx["Flash"] = f
|
||||||
|
}
|
||||||
|
ctx["CSRFToken"] = nosurf.Token(c.Request)
|
||||||
|
|
||||||
|
// Rehydrate maps (not structs) from session for sticky form + field errors
|
||||||
|
if v := sm.Pop(c.Request.Context(), "register.form"); v != nil {
|
||||||
|
if fm, ok := v.(map[string]string); ok {
|
||||||
|
ctx["Form"] = fm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := sm.Pop(c.Request.Context(), "register.errors"); v != nil {
|
||||||
|
if errs, ok := v.(map[string]string); ok {
|
||||||
|
ctx["Errors"] = errs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// layout-first, finalized path
|
||||||
|
tmpl := templateHelpers.LoadTemplateFiles(
|
||||||
|
"web/templates/layout.html",
|
||||||
|
"web/templates/account/signup.html",
|
||||||
|
)
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil {
|
||||||
|
logging.Info("❌ Template render error (register): %v", err)
|
||||||
|
c.String(http.StatusInternalServerError, "Error rendering register page")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SignupPost(c *gin.Context) {
|
||||||
|
app := c.MustGet("app").(*bootstrap.App)
|
||||||
|
sm := app.SessionManager
|
||||||
|
db := app.DB
|
||||||
|
|
||||||
|
r := c.Request
|
||||||
|
|
||||||
|
form := registerForm{
|
||||||
|
Username: strings.TrimSpace(r.FormValue("username")),
|
||||||
|
Email: strings.TrimSpace(r.FormValue("email")),
|
||||||
|
Password: r.FormValue("password"),
|
||||||
|
PasswordConfirm: r.FormValue("password_confirm"),
|
||||||
|
AcceptTerms: r.FormValue("accept_terms") == "on",
|
||||||
|
}
|
||||||
|
|
||||||
|
errors := validateRegisterForm(db, form)
|
||||||
|
if len(errors) > 0 {
|
||||||
|
// ✅ Stash maps instead of a struct → gob-safe with SCS
|
||||||
|
formMap := map[string]string{
|
||||||
|
"username": form.Username,
|
||||||
|
"email": form.Email,
|
||||||
|
"accept_terms": func() string {
|
||||||
|
if form.AcceptTerms {
|
||||||
|
return "on"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}(),
|
||||||
|
}
|
||||||
|
sm.Put(r.Context(), "register.form", formMap)
|
||||||
|
sm.Put(r.Context(), "register.errors", errors)
|
||||||
|
sm.Put(r.Context(), "flash", "Please fix the highlighted errors.")
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/account/signup")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
hash, err := securityHelpers.HashPassword(form.Password)
|
||||||
|
if err != nil {
|
||||||
|
logging.Info("❌ Hash error: %v", err)
|
||||||
|
sm.Put(r.Context(), "flash", "Something went wrong. Please try again.")
|
||||||
|
c.Redirect(http.StatusSeeOther, "/account/signup")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
id, err := usersStorage.CreateUser(db, form.Username, form.Email, hash)
|
||||||
|
if err != nil {
|
||||||
|
logging.Info("❌ CreateUser error: %v", err)
|
||||||
|
// Unique constraints might still trip here
|
||||||
|
sm.Put(r.Context(), "flash", "That username or email is already taken.")
|
||||||
|
c.Redirect(http.StatusSeeOther, "/account/signup")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audit registration
|
||||||
|
auditlogStorage.LogSignup(
|
||||||
|
db,
|
||||||
|
id,
|
||||||
|
form.Username,
|
||||||
|
form.Email,
|
||||||
|
httphelpers.ClientIP(r),
|
||||||
|
r.UserAgent(),
|
||||||
|
)
|
||||||
|
|
||||||
|
sm.Put(r.Context(), "flash", "Account created. You can log in now.")
|
||||||
|
c.Redirect(http.StatusSeeOther, "/account/login")
|
||||||
|
c.Abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateRegisterForm(db *sql.DB, f registerForm) map[string]string {
|
||||||
|
errs := make(map[string]string)
|
||||||
|
|
||||||
|
if f.Username == "" || len(f.Username) < 3 {
|
||||||
|
errs["username"] = "Username must be at least 3 characters."
|
||||||
|
} else if usersStorage.UsernameExists(db, f.Username) {
|
||||||
|
errs["username"] = "Username is already in use."
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.Email == "" || !looksLikeEmail(f.Email) {
|
||||||
|
errs["email"] = "Please enter a valid email."
|
||||||
|
} else if usersStorage.EmailExists(db, f.Email) {
|
||||||
|
errs["email"] = "Email is already registered."
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(f.Password) < 8 {
|
||||||
|
errs["password"] = "Password must be at least 8 characters."
|
||||||
|
}
|
||||||
|
if f.Password != f.PasswordConfirm {
|
||||||
|
errs["password_confirm"] = "Passwords do not match."
|
||||||
|
}
|
||||||
|
if !f.AcceptTerms {
|
||||||
|
errs["accept_terms"] = "You must accept the terms."
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func looksLikeEmail(s string) bool {
|
||||||
|
// Keep it simple; you can swap for a stricter validator later
|
||||||
|
return strings.Count(s, "@") == 1 && strings.Contains(s, ".")
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
|
|
||||||
templateHelpers "synlotto-website/internal/helpers/template"
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
|
||||||
"synlotto-website/internal/http/middleware"
|
|
||||||
"synlotto-website/internal/models"
|
"synlotto-website/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,7 +19,7 @@ type AdminLogEntry struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func AdminAccessLogHandler(db *sql.DB) http.HandlerFunc {
|
func AdminAccessLogHandler(db *sql.DB) http.HandlerFunc {
|
||||||
return middleware.Auth(true)(func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
data := models.TemplateData{}
|
data := models.TemplateData{}
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
context := templateHelpers.TemplateContext(w, r, data)
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ func AdminAccessLogHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var logs []AdminLogEntry // ToDo should be in models
|
var logs []AdminLogEntry // ToDo: move to models ?
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var entry AdminLogEntry
|
var entry AdminLogEntry
|
||||||
if err := rows.Scan(&entry.AccessedAt, &entry.UserID, &entry.Path, &entry.IP, &entry.UserAgent); err != nil {
|
if err := rows.Scan(&entry.AccessedAt, &entry.UserID, &entry.Path, &entry.IP, &entry.UserAgent); err != nil {
|
||||||
@@ -48,14 +47,13 @@ func AdminAccessLogHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
context["AuditLogs"] = logs
|
context["AuditLogs"] = logs
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("access_log.html", "templates/admin/logs/access_log.html")
|
tmpl := templateHelpers.LoadTemplateFiles("access_log.html", "web/templates/admin/logs/access_log.html")
|
||||||
|
|
||||||
_ = tmpl.ExecuteTemplate(w, "layout", context)
|
_ = tmpl.ExecuteTemplate(w, "layout", context)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AuditLogHandler(db *sql.DB) http.HandlerFunc {
|
func AuditLogHandler(db *sql.DB) http.HandlerFunc {
|
||||||
return middleware.Auth(true)(func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
data := models.TemplateData{}
|
data := models.TemplateData{}
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
context := templateHelpers.TemplateContext(w, r, data)
|
||||||
|
|
||||||
@@ -75,8 +73,7 @@ func AuditLogHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
var logs []models.AuditEntry
|
var logs []models.AuditEntry
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var entry models.AuditEntry
|
var entry models.AuditEntry
|
||||||
err := rows.Scan(&entry.Timestamp, &entry.UserID, &entry.Action, &entry.IP, &entry.UserAgent)
|
if err := rows.Scan(&entry.Timestamp, &entry.UserID, &entry.Action, &entry.IP, &entry.UserAgent); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Println("⚠️ Failed to scan row:", err)
|
log.Println("⚠️ Failed to scan row:", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -85,12 +82,10 @@ func AuditLogHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
|
|
||||||
context["AuditLogs"] = logs
|
context["AuditLogs"] = logs
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("audit.html", "templates/admin/logs/audit.html")
|
tmpl := templateHelpers.LoadTemplateFiles("audit.html", "web/templates/admin/logs/audit.html")
|
||||||
|
if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil {
|
||||||
err = tmpl.ExecuteTemplate(w, "layout", context)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("❌ Failed to render audit page:", err)
|
log.Println("❌ Failed to render audit page:", err)
|
||||||
http.Error(w, "Template error", http.StatusInternalServerError)
|
http.Error(w, "Template error", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +1,96 @@
|
|||||||
|
// internal/handlers/admin/dashboard.go
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
|
// ToDo: move SQL into storage layer
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
httpHelpers "synlotto-website/internal/helpers/http"
|
templateHandlers "synlotto-website/internal/handlers/template"
|
||||||
securityHelpers "synlotto-website/internal/helpers/security"
|
security "synlotto-website/internal/helpers/security"
|
||||||
templateHelpers "synlotto-website/internal/helpers/template"
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
"synlotto-website/internal/models"
|
usersStorage "synlotto-website/internal/storage/users"
|
||||||
"synlotto-website/internal/storage"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func AdminDashboardHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
total, winners int
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
prizeSum float64
|
userID, ok := security.GetCurrentUserID(app.SessionManager, r)
|
||||||
)
|
|
||||||
|
|
||||||
func AdminDashboardHandler(db *sql.DB) http.HandlerFunc {
|
|
||||||
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user := storage.GetUserByID(db, userID)
|
user := usersStorage.GetUserByID(app.DB, userID)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
http.Error(w, "User not found", http.StatusUnauthorized)
|
http.Error(w, "User not found", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data := models.TemplateData{}
|
// Shared template data (loads user, notifications, counts, etc.)
|
||||||
|
data := templateHandlers.BuildTemplateData(app, w, r)
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
context := templateHelpers.TemplateContext(w, r, data)
|
||||||
context["User"] = user
|
context["User"] = user
|
||||||
context["IsAdmin"] = user.IsAdmin
|
context["IsAdmin"] = user.IsAdmin
|
||||||
// Missing messages, notifications, potentially syndicate notifictions if that becomes a new top bar icon.
|
|
||||||
db.QueryRow(`SELECT COUNT(*), SUM(CASE WHEN is_winner THEN 1 ELSE 0 END), SUM(prize_amount) FROM my_tickets`).Scan(&total, &winners, &prizeSum)
|
// Quick stats (keep here for now; move to storage soon)
|
||||||
|
var (
|
||||||
|
total, winners int
|
||||||
|
prizeSum float64
|
||||||
|
)
|
||||||
|
if err := app.DB.QueryRow(`
|
||||||
|
SELECT COUNT(*),
|
||||||
|
SUM(CASE WHEN is_winner THEN 1 ELSE 0 END),
|
||||||
|
COALESCE(SUM(prize_amount), 0)
|
||||||
|
FROM my_tickets
|
||||||
|
`).Scan(&total, &winners, &prizeSum); err != nil {
|
||||||
|
log.Println("⚠️ Failed to load ticket stats:", err)
|
||||||
|
}
|
||||||
context["Stats"] = map[string]interface{}{
|
context["Stats"] = map[string]interface{}{
|
||||||
"TotalTickets": total,
|
"TotalTickets": total,
|
||||||
"TotalWinners": winners,
|
"TotalWinners": winners,
|
||||||
"TotalPrizeAmount": prizeSum,
|
"TotalPrizeAmount": prizeSum,
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := db.Query(`
|
// Recent matcher logs (limit 10)
|
||||||
|
rows, err := app.DB.Query(`
|
||||||
SELECT run_at, triggered_by, tickets_matched, winners_found, COALESCE(notes, '')
|
SELECT run_at, triggered_by, tickets_matched, winners_found, COALESCE(notes, '')
|
||||||
FROM log_ticket_matching
|
FROM log_ticket_matching
|
||||||
ORDER BY run_at DESC LIMIT 10
|
ORDER BY run_at DESC
|
||||||
|
LIMIT 10
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("⚠️ Failed to load logs:", err)
|
log.Println("⚠️ Failed to load logs:", err)
|
||||||
}
|
} else {
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
var logs []struct {
|
||||||
var logs []models.MatchLog
|
RunAt any
|
||||||
|
TriggeredBy string
|
||||||
|
TicketsMatched int
|
||||||
|
WinnersFound int
|
||||||
|
Notes string
|
||||||
|
}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var logEntry models.MatchLog
|
var e struct {
|
||||||
err := rows.Scan(&logEntry.RunAt, &logEntry.TriggeredBy, &logEntry.TicketsMatched, &logEntry.WinnersFound, &logEntry.Notes)
|
RunAt any
|
||||||
if err != nil {
|
TriggeredBy string
|
||||||
|
TicketsMatched int
|
||||||
|
WinnersFound int
|
||||||
|
Notes string
|
||||||
|
}
|
||||||
|
if err := rows.Scan(&e.RunAt, &e.TriggeredBy, &e.TicketsMatched, &e.WinnersFound, &e.Notes); err != nil {
|
||||||
log.Println("⚠️ Failed to scan log row:", err)
|
log.Println("⚠️ Failed to scan log row:", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
logs = append(logs, logEntry)
|
logs = append(logs, e)
|
||||||
}
|
}
|
||||||
context["MatchLogs"] = logs
|
context["MatchLogs"] = logs
|
||||||
|
}
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("dashboard.html", "templates/admin/dashboard.html")
|
tmpl := templateHelpers.LoadTemplateFiles("dashboard.html", "web/templates/admin/dashboard.html")
|
||||||
|
if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil {
|
||||||
err = tmpl.ExecuteTemplate(w, "layout", context)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to render dashboard", http.StatusInternalServerError)
|
http.Error(w, "Failed to render dashboard", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
|
// ToDo: move SQL into storage layer
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
httpHelpers "synlotto-website/internal/helpers/http"
|
|
||||||
templateHelpers "synlotto-website/internal/helpers/template"
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
|
||||||
"synlotto-website/internal/models"
|
"synlotto-website/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewDrawHandler(db *sql.DB) http.HandlerFunc {
|
func NewDrawHandler(db *sql.DB) http.HandlerFunc {
|
||||||
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
data := models.TemplateData{}
|
data := models.TemplateData{}
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
ctx := templateHelpers.TemplateContext(w, r, data)
|
||||||
|
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
game := r.FormValue("game_type")
|
game := r.FormValue("game_type")
|
||||||
@@ -22,29 +21,35 @@ func NewDrawHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
machine := r.FormValue("machine")
|
machine := r.FormValue("machine")
|
||||||
ballset := r.FormValue("ball_set")
|
ballset := r.FormValue("ball_set")
|
||||||
|
|
||||||
_, err := db.Exec(`INSERT INTO results_thunderball (game_type, draw_date, machine, ball_set) VALUES (?, ?, ?, ?)`,
|
_, err := db.Exec(
|
||||||
game, date, machine, ballset)
|
`INSERT INTO results_thunderball (game_type, draw_date, machine, ball_set) VALUES (?, ?, ?, ?)`,
|
||||||
|
game, date, machine, ballset,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Failed to add draw", http.StatusInternalServerError)
|
http.Error(w, "Failed to add draw", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, "/admin/dashboard", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/dashboard", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("new_draw", "templates/admin/draws/new_draw.html")
|
tmpl := templateHelpers.LoadTemplateFiles("new_draw", "web/templates/admin/draws/new_draw.html")
|
||||||
|
_ = tmpl.ExecuteTemplate(w, "layout", ctx)
|
||||||
tmpl.ExecuteTemplate(w, "layout", context)
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ModifyDrawHandler(db *sql.DB) http.HandlerFunc {
|
func ModifyDrawHandler(db *sql.DB) http.HandlerFunc {
|
||||||
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
id := r.FormValue("id")
|
id := r.FormValue("id")
|
||||||
_, err := db.Exec(`UPDATE results_thunderball SET game_type=?, draw_date=?, ball_set=?, machine=? WHERE id=?`,
|
_, err := db.Exec(
|
||||||
r.FormValue("game_type"), r.FormValue("draw_date"), r.FormValue("ball_set"), r.FormValue("machine"), id)
|
`UPDATE results_thunderball SET game_type=?, draw_date=?, ball_set=?, machine=? WHERE id=?`,
|
||||||
|
r.FormValue("game_type"),
|
||||||
|
r.FormValue("draw_date"),
|
||||||
|
r.FormValue("ball_set"),
|
||||||
|
r.FormValue("machine"),
|
||||||
|
id,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Update failed", http.StatusInternalServerError)
|
http.Error(w, "Update failed", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -52,33 +57,30 @@ func ModifyDrawHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
http.Redirect(w, r, "/admin/dashboard", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/dashboard", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// For GET: load draw by ID (pseudo-code)
|
// For GET: load draw by ID if needed and render a form/template
|
||||||
// id := r.URL.Query().Get("id")
|
}
|
||||||
// query DB, pass into context.Draw
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteDrawHandler(db *sql.DB) http.HandlerFunc {
|
func DeleteDrawHandler(db *sql.DB) http.HandlerFunc {
|
||||||
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
id := r.FormValue("id")
|
id := r.FormValue("id")
|
||||||
_, err := db.Exec(`DELETE FROM results_thunderball WHERE id = ?`, id)
|
if _, err := db.Exec(`DELETE FROM results_thunderball WHERE id = ?`, id); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Delete failed", http.StatusInternalServerError)
|
http.Error(w, "Delete failed", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, "/admin/dashboard", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/dashboard", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListDrawsHandler(db *sql.DB) http.HandlerFunc {
|
func ListDrawsHandler(db *sql.DB) http.HandlerFunc {
|
||||||
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
data := models.TemplateData{}
|
data := models.TemplateData{}
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
ctx := templateHelpers.TemplateContext(w, r, data)
|
||||||
draws := []models.DrawSummary{}
|
|
||||||
|
|
||||||
|
var draws []models.DrawSummary
|
||||||
rows, err := db.Query(`
|
rows, err := db.Query(`
|
||||||
SELECT r.id, r.game_type, r.draw_date, r.ball_set, r.machine,
|
SELECT r.id, r.game_type, r.draw_date, r.ball_set, r.machine,
|
||||||
(SELECT COUNT(1) FROM prizes_thunderball p WHERE p.draw_date = r.draw_date) as prize_exists
|
(SELECT COUNT(1) FROM prizes_thunderball p WHERE p.draw_date = r.draw_date) as prize_exists
|
||||||
@@ -101,11 +103,9 @@ func ListDrawsHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
d.PrizeSet = prizeFlag > 0
|
d.PrizeSet = prizeFlag > 0
|
||||||
draws = append(draws, d)
|
draws = append(draws, d)
|
||||||
}
|
}
|
||||||
|
ctx["Draws"] = draws
|
||||||
|
|
||||||
context["Draws"] = draws
|
tmpl := templateHelpers.LoadTemplateFiles("list.html", "web/templates/admin/draws/list.html")
|
||||||
|
_ = tmpl.ExecuteTemplate(w, "layout", ctx)
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("list.html", "templates/admin/draws/list.html")
|
}
|
||||||
|
|
||||||
tmpl.ExecuteTemplate(w, "layout", context)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"synlotto-website/internal/models"
|
"synlotto-website/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ToDo: need to fix flash messages from new gin context
|
||||||
func AdminTriggersHandler(db *sql.DB) http.HandlerFunc {
|
func AdminTriggersHandler(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
data := models.TemplateData{}
|
data := models.TemplateData{}
|
||||||
@@ -73,7 +74,7 @@ func AdminTriggersHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("triggers.html", "templates/admin/triggers.html")
|
tmpl := templateHelpers.LoadTemplateFiles("triggers.html", "web/templates/admin/triggers.html")
|
||||||
|
|
||||||
err := tmpl.ExecuteTemplate(w, "layout", context)
|
err := tmpl.ExecuteTemplate(w, "layout", context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -6,23 +6,23 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
httpHelpers "synlotto-website/internal/helpers/http"
|
|
||||||
templateHelpers "synlotto-website/internal/helpers/template"
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
|
||||||
"synlotto-website/internal/models"
|
"synlotto-website/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ToDo: move SQL into the storage layer.
|
||||||
|
|
||||||
func AddPrizesHandler(db *sql.DB) http.HandlerFunc {
|
func AddPrizesHandler(db *sql.DB) http.HandlerFunc {
|
||||||
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
data := models.TemplateData{}
|
data := models.TemplateData{}
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("add_prizes.html", "templates/admin/draws/prizes/add_prizes.html")
|
tmpl := templateHelpers.LoadTemplateFiles("add_prizes.html", "web/templates/admin/draws/prizes/add_prizes.html")
|
||||||
tmpl.ExecuteTemplate(w, "layout", templateHelpers.TemplateContext(w, r, data))
|
_ = tmpl.ExecuteTemplate(w, "layout", templateHelpers.TemplateContext(w, r, data))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
drawDate := r.FormValue("draw_date")
|
drawDate := r.FormValue("draw_date")
|
||||||
values := make([]interface{}, 0)
|
values := make([]interface{}, 0, 9)
|
||||||
for i := 1; i <= 9; i++ {
|
for i := 1; i <= 9; i++ {
|
||||||
val, _ := strconv.Atoi(r.FormValue(fmt.Sprintf("prize%d_per_winner", i)))
|
val, _ := strconv.Atoi(r.FormValue(fmt.Sprintf("prize%d_per_winner", i)))
|
||||||
values = append(values, val)
|
values = append(values, val)
|
||||||
@@ -34,23 +34,21 @@ func AddPrizesHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
prize7_per_winner, prize8_per_winner, prize9_per_winner
|
prize7_per_winner, prize8_per_winner, prize9_per_winner
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
|
|
||||||
_, err := db.Exec(stmt, append([]interface{}{drawDate}, values...)...)
|
if _, err := db.Exec(stmt, append([]interface{}{drawDate}, values...)...); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Insert failed: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "Insert failed: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, "/admin/draws", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/draws", http.StatusSeeOther)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ModifyPrizesHandler(db *sql.DB) http.HandlerFunc {
|
func ModifyPrizesHandler(db *sql.DB) http.HandlerFunc {
|
||||||
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
data := models.TemplateData{}
|
data := models.TemplateData{}
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("modify_prizes.html", "templates/admin/draws/prizes/modify_prizes.html")
|
tmpl := templateHelpers.LoadTemplateFiles("modify_prizes.html", "web/templates/admin/draws/prizes/modify_prizes.html")
|
||||||
|
_ = tmpl.ExecuteTemplate(w, "layout", templateHelpers.TemplateContext(w, r, data))
|
||||||
tmpl.ExecuteTemplate(w, "layout", templateHelpers.TemplateContext(w, r, data))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,13 +56,12 @@ func ModifyPrizesHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
for i := 1; i <= 9; i++ {
|
for i := 1; i <= 9; i++ {
|
||||||
key := fmt.Sprintf("prize%d_per_winner", i)
|
key := fmt.Sprintf("prize%d_per_winner", i)
|
||||||
val, _ := strconv.Atoi(r.FormValue(key))
|
val, _ := strconv.Atoi(r.FormValue(key))
|
||||||
_, err := db.Exec("UPDATE prizes_thunderball SET "+key+" = ? WHERE draw_date = ?", val, drawDate)
|
if _, err := db.Exec("UPDATE prizes_thunderball SET "+key+" = ? WHERE draw_date = ?", val, drawDate); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Update failed: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "Update failed: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, "/admin/draws", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/draws", http.StatusSeeOther)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
templateHandlers "synlotto-website/internal/handlers/template"
|
templateHandlers "synlotto-website/internal/handlers/template"
|
||||||
templateHelpers "synlotto-website/internal/helpers/template"
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Home(db *sql.DB) http.HandlerFunc {
|
func Home(app *bootstrap.App) gin.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(c *gin.Context) {
|
||||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
data := templateHandlers.BuildTemplateData(app, c.Writer, c.Request)
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, data)
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("index.html", "templates/index.html")
|
tmpl := templateHelpers.LoadTemplateFiles("layout.html", "web/templates/index.html")
|
||||||
|
|
||||||
err := tmpl.ExecuteTemplate(w, "layout", context)
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||||
if err != nil {
|
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil {
|
||||||
log.Println("❌ Template render error:", err)
|
log.Println("❌ Template render error:", err)
|
||||||
http.Error(w, "Error rendering homepage", http.StatusInternalServerError)
|
c.String(http.StatusInternalServerError, "Template render error: %v", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
templateHelpers "synlotto-website/internal/helpers/template"
|
|
||||||
|
|
||||||
"synlotto-website/internal/helpers"
|
"synlotto-website/internal/helpers"
|
||||||
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
"synlotto-website/internal/models"
|
"synlotto-website/internal/models"
|
||||||
"synlotto-website/internal/storage"
|
resultsThunderballStorage "synlotto-website/internal/storage/results/thunderball"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewDraw(db *sql.DB) http.HandlerFunc {
|
func NewDraw(db *sql.DB) http.HandlerFunc {
|
||||||
@@ -19,7 +18,7 @@ func NewDraw(db *sql.DB) http.HandlerFunc {
|
|||||||
context["Page"] = "new_draw"
|
context["Page"] = "new_draw"
|
||||||
context["Data"] = nil
|
context["Data"] = nil
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("new_draw.html", "templates/admin/draws/new_draw.html") // ToDo: may need removing or moving add draw should be admin functionality and only when manually required. Potential live drawing of numbers in the future.
|
tmpl := templateHelpers.LoadTemplateFiles("new_draw.html", "web/templates/admin/draws/new_draw.html") // ToDo: may need removing or moving add draw should be admin functionality and only when manually required. Potential live drawing of numbers in the future.
|
||||||
|
|
||||||
err := tmpl.ExecuteTemplate(w, "layout", context)
|
err := tmpl.ExecuteTemplate(w, "layout", context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -45,7 +44,7 @@ func Submit(db *sql.DB, w http.ResponseWriter, r *http.Request) {
|
|||||||
Thunderball: helpers.Atoi(r.FormValue("thunderball")),
|
Thunderball: helpers.Atoi(r.FormValue("thunderball")),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := storage.InsertThunderballResult(db, draw)
|
err := resultsThunderballStorage.InsertThunderballResult(db, draw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("❌ Failed to insert draw:", err)
|
log.Println("❌ Failed to insert draw:", err)
|
||||||
http.Error(w, "Failed to save draw", http.StatusInternalServerError)
|
http.Error(w, "Failed to save draw", http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
// internal/handlers/lottery/syndicate/syndicate.go
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -9,38 +9,39 @@ import (
|
|||||||
templateHandlers "synlotto-website/internal/handlers/template"
|
templateHandlers "synlotto-website/internal/handlers/template"
|
||||||
securityHelpers "synlotto-website/internal/helpers/security"
|
securityHelpers "synlotto-website/internal/helpers/security"
|
||||||
templateHelpers "synlotto-website/internal/helpers/template"
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
syndicateStorage "synlotto-website/internal/storage/syndicate"
|
||||||
|
ticketStorage "synlotto-website/internal/storage/tickets"
|
||||||
|
|
||||||
"synlotto-website/internal/helpers"
|
"synlotto-website/internal/helpers"
|
||||||
"synlotto-website/internal/models"
|
"synlotto-website/internal/models"
|
||||||
"synlotto-website/internal/storage"
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateSyndicateHandler(db *sql.DB) http.HandlerFunc {
|
func CreateSyndicateHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
data := templateHandlers.BuildTemplateData(app, w, r)
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
ctx := templateHelpers.TemplateContext(w, r, data)
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("create-syndicate.html", "templates/syndicate/create.html")
|
tmpl := templateHelpers.LoadTemplateFiles("create-syndicate.html", "web/templates/syndicate/create.html")
|
||||||
tmpl.ExecuteTemplate(w, "layout", context)
|
_ = tmpl.ExecuteTemplate(w, "layout", ctx)
|
||||||
|
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
name := r.FormValue("name")
|
name := r.FormValue("name")
|
||||||
description := r.FormValue("description")
|
description := r.FormValue("description")
|
||||||
|
|
||||||
userId, ok := securityHelpers.GetCurrentUserID(r)
|
userId, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok || name == "" {
|
if !ok || name == "" {
|
||||||
templateHelpers.SetFlash(w, r, "Invalid data submitted")
|
templateHelpers.SetFlash(r, "Invalid data submitted")
|
||||||
http.Redirect(w, r, "/syndicate/create", http.StatusSeeOther)
|
http.Redirect(w, r, "/syndicate/create", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := storage.CreateSyndicate(db, userId, name, description)
|
if _, err := syndicateStorage.CreateSyndicate(app.DB, userId, name, description); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Printf("❌ CreateSyndicate failed: %v", err)
|
log.Printf("❌ CreateSyndicate failed: %v", err)
|
||||||
templateHelpers.SetFlash(w, r, "Failed to create syndicate")
|
templateHelpers.SetFlash(r, "Failed to create syndicate")
|
||||||
} else {
|
} else {
|
||||||
templateHelpers.SetFlash(w, r, "Syndicate created successfully")
|
templateHelpers.SetFlash(r, "Syndicate created successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
|
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
|
||||||
@@ -50,18 +51,18 @@ func CreateSyndicateHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListSyndicatesHandler(db *sql.DB) http.HandlerFunc {
|
func ListSyndicatesHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
templateHelpers.RenderError(w, r, 403) // ToDo need to make this use the handler so i dont need to define errors.
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
managed := storage.GetSyndicatesByOwner(db, userID)
|
managed := syndicateStorage.GetSyndicatesByOwner(app.DB, userID)
|
||||||
member := storage.GetSyndicatesByMember(db, userID)
|
member := syndicateStorage.GetSyndicatesByMember(app.DB, userID)
|
||||||
|
|
||||||
managedMap := make(map[int]bool)
|
managedMap := make(map[int]bool, len(managed))
|
||||||
for _, s := range managed {
|
for _, s := range managed {
|
||||||
managedMap[s.ID] = true
|
managedMap[s.ID] = true
|
||||||
}
|
}
|
||||||
@@ -73,131 +74,131 @@ func ListSyndicatesHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
data := templateHandlers.BuildTemplateData(app, w, r)
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
ctx := templateHelpers.TemplateContext(w, r, data)
|
||||||
context["ManagedSyndicates"] = managed
|
ctx["ManagedSyndicates"] = managed
|
||||||
context["JoinedSyndicates"] = filteredJoined
|
ctx["JoinedSyndicates"] = filteredJoined
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("syndicates.html", "templates/syndicate/index.html")
|
tmpl := templateHelpers.LoadTemplateFiles("syndicates.html", "web/templates/syndicate/index.html")
|
||||||
tmpl.ExecuteTemplate(w, "layout", context)
|
_ = tmpl.ExecuteTemplate(w, "layout", ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ViewSyndicateHandler(db *sql.DB) http.HandlerFunc {
|
func ViewSyndicateHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
templateHelpers.RenderError(w, r, 403)
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||||
syndicate, err := storage.GetSyndicateByID(db, syndicateID)
|
syndicate, err := syndicateStorage.GetSyndicateByID(app.DB, syndicateID)
|
||||||
if err != nil || syndicate == nil {
|
if err != nil || syndicate == nil {
|
||||||
templateHelpers.RenderError(w, r, 404)
|
templateHelpers.RenderError(w, r, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isManager := userID == syndicate.OwnerID
|
isManager := userID == syndicate.OwnerID
|
||||||
isMember := storage.IsSyndicateMember(db, syndicateID, userID)
|
isMember := syndicateStorage.IsSyndicateMember(app.DB, syndicateID, userID)
|
||||||
|
|
||||||
if !isManager && !isMember {
|
if !isManager && !isMember {
|
||||||
templateHelpers.RenderError(w, r, 403)
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
members := storage.GetSyndicateMembers(db, syndicateID)
|
members := syndicateStorage.GetSyndicateMembers(app.DB, syndicateID)
|
||||||
|
|
||||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
data := templateHandlers.BuildTemplateData(app, w, r)
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
ctx := templateHelpers.TemplateContext(w, r, data)
|
||||||
context["Syndicate"] = syndicate
|
ctx["Syndicate"] = syndicate
|
||||||
context["Members"] = members
|
ctx["Members"] = members
|
||||||
context["IsManager"] = isManager
|
ctx["IsManager"] = isManager
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("syndicate-view.html", "templates/syndicate/view.html")
|
tmpl := templateHelpers.LoadTemplateFiles("syndicate-view.html", "web/templates/syndicate/view.html")
|
||||||
tmpl.ExecuteTemplate(w, "layout", context)
|
_ = tmpl.ExecuteTemplate(w, "layout", ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SyndicateLogTicketHandler(db *sql.DB) http.HandlerFunc {
|
func SyndicateLogTicketHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
templateHelpers.RenderError(w, r, 403)
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
syndicateId := helpers.Atoi(r.URL.Query().Get("id"))
|
syndicateId := helpers.Atoi(r.URL.Query().Get("id"))
|
||||||
syndicate, err := storage.GetSyndicateByID(db, syndicateId)
|
syndicate, err := syndicateStorage.GetSyndicateByID(app.DB, syndicateId)
|
||||||
if err != nil || syndicate.OwnerID != userID {
|
if err != nil || syndicate.OwnerID != userID {
|
||||||
templateHelpers.RenderError(w, r, 403)
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
data := templateHandlers.BuildTemplateData(app, w, r)
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
ctx := templateHelpers.TemplateContext(w, r, data)
|
||||||
context["Syndicate"] = syndicate
|
ctx["Syndicate"] = syndicate
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("syndicate-log-ticket.html", "templates/syndicate/log_ticket.html")
|
tmpl := templateHelpers.LoadTemplateFiles("syndicate-log-ticket.html", "web/templates/syndicate/log_ticket.html")
|
||||||
tmpl.ExecuteTemplate(w, "layout", context)
|
_ = tmpl.ExecuteTemplate(w, "layout", ctx)
|
||||||
|
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
gameType := r.FormValue("game_type")
|
gameType := r.FormValue("game_type")
|
||||||
drawDate := r.FormValue("draw_date")
|
drawDate := r.FormValue("draw_date")
|
||||||
method := r.FormValue("purchase_method")
|
method := r.FormValue("purchase_method")
|
||||||
|
|
||||||
err := storage.InsertTicket(db, models.Ticket{
|
err := ticketStorage.InsertTicket(app.DB, models.Ticket{
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
GameType: gameType,
|
GameType: gameType,
|
||||||
DrawDate: drawDate,
|
DrawDate: drawDate,
|
||||||
PurchaseMethod: method,
|
PurchaseMethod: method,
|
||||||
SyndicateId: &syndicateId,
|
SyndicateId: &syndicateId,
|
||||||
// ToDo image path
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
templateHelpers.SetFlash(w, r, "Failed to add ticket.")
|
templateHelpers.SetFlash(r, "Failed to add ticket.")
|
||||||
} else {
|
} else {
|
||||||
templateHelpers.SetFlash(w, r, "Ticket added for syndicate.")
|
templateHelpers.SetFlash(r, "Ticket added for syndicate.")
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, fmt.Sprintf("/syndicate/view?id=%d", syndicateId), http.StatusSeeOther)
|
http.Redirect(w, r, fmt.Sprintf("/syndicate/view?id=%d", syndicateId), http.StatusSeeOther)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
templateHelpers.RenderError(w, r, 405)
|
templateHelpers.RenderError(w, r, http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SyndicateTicketsHandler(db *sql.DB) http.HandlerFunc {
|
func SyndicateTicketsHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
templateHelpers.RenderError(w, r, 403)
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||||
if syndicateID == 0 {
|
if syndicateID == 0 {
|
||||||
templateHelpers.RenderError(w, r, 400)
|
templateHelpers.RenderError(w, r, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !storage.IsSyndicateMember(db, syndicateID, userID) {
|
if !syndicateStorage.IsSyndicateMember(app.DB, syndicateID, userID) {
|
||||||
templateHelpers.RenderError(w, r, 403)
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tickets := storage.GetSyndicateTickets(db, syndicateID)
|
// You said GetSyndicateTickets lives in storage/syndicate:
|
||||||
|
tickets := syndicateStorage.GetSyndicateTickets(app.DB, syndicateID)
|
||||||
|
// If you later move it into tickets storage, switch to:
|
||||||
|
// tickets := ticketStorage.GetSyndicateTickets(app.DB, syndicateID)
|
||||||
|
|
||||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
data := templateHandlers.BuildTemplateData(app, w, r)
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
ctx := templateHelpers.TemplateContext(w, r, data)
|
||||||
context["SyndicateID"] = syndicateID
|
ctx["SyndicateID"] = syndicateID
|
||||||
context["Tickets"] = tickets
|
ctx["Tickets"] = tickets
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("syndicate-tickets.html", "templates/syndicate/tickets.html")
|
tmpl := templateHelpers.LoadTemplateFiles("syndicate-tickets.html", "web/templates/syndicate/tickets.html")
|
||||||
tmpl.ExecuteTemplate(w, "layout", context)
|
_ = tmpl.ExecuteTemplate(w, "layout", ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
|
// internal/handlers/lottery/syndicate/syndicate_invites.go
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
templateHandlers "synlotto-website/internal/handlers/template"
|
templateHandlers "synlotto-website/internal/handlers/template"
|
||||||
|
"synlotto-website/internal/helpers"
|
||||||
securityHelpers "synlotto-website/internal/helpers/security"
|
securityHelpers "synlotto-website/internal/helpers/security"
|
||||||
templateHelpers "synlotto-website/internal/helpers/template"
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
"synlotto-website/internal/helpers"
|
syndicateStorage "synlotto-website/internal/storage/syndicate"
|
||||||
"synlotto-website/internal/storage"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func SyndicateInviteHandler(db *sql.DB) http.HandlerFunc {
|
// GET /syndicate/invite?id=<syndicate_id>
|
||||||
|
// POST /syndicate/invite (syndicate_id, username)
|
||||||
|
func SyndicateInviteHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
@@ -26,24 +28,23 @@ func SyndicateInviteHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
data := templateHandlers.BuildTemplateData(app, w, r)
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
ctx := templateHelpers.TemplateContext(w, r, data)
|
||||||
context["SyndicateID"] = syndicateID
|
ctx["SyndicateID"] = syndicateID
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("invite-syndicate.html", "templates/syndicate/invite.html")
|
tmpl := templateHelpers.LoadTemplateFiles("invite-syndicate.html", "web/templates/syndicate/invite.html")
|
||||||
err := tmpl.ExecuteTemplate(w, "layout", context)
|
if err := tmpl.ExecuteTemplate(w, "layout", ctx); err != nil {
|
||||||
if err != nil {
|
templateHelpers.RenderError(w, r, http.StatusInternalServerError)
|
||||||
templateHelpers.RenderError(w, r, 500)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
syndicateID := helpers.Atoi(r.FormValue("syndicate_id"))
|
syndicateID := helpers.Atoi(r.FormValue("syndicate_id"))
|
||||||
username := r.FormValue("username")
|
username := r.FormValue("username")
|
||||||
|
|
||||||
err := storage.InviteToSyndicate(db, userID, syndicateID, username)
|
if err := syndicateStorage.InviteToSyndicate(app.DB, userID, syndicateID, username); err != nil {
|
||||||
if err != nil {
|
templateHelpers.SetFlash(r, "Failed to send invite: "+err.Error())
|
||||||
templateHelpers.SetFlash(w, r, "Failed to send invite: "+err.Error())
|
|
||||||
} else {
|
} else {
|
||||||
templateHelpers.SetFlash(w, r, "Invite sent successfully.")
|
templateHelpers.SetFlash(r, "Invite sent successfully.")
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther)
|
http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther)
|
||||||
|
|
||||||
@@ -53,90 +54,104 @@ func SyndicateInviteHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ViewInvitesHandler(db *sql.DB) http.HandlerFunc {
|
// GET /syndicate/invites
|
||||||
|
func ViewInvitesHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
templateHelpers.RenderError(w, r, 403)
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
invites := storage.GetPendingInvites(db, userID)
|
invites := syndicateStorage.GetPendingSyndicateInvites(app.DB, userID)
|
||||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
|
||||||
context["Invites"] = invites
|
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("invites.html", "templates/syndicate/invites.html")
|
data := templateHandlers.BuildTemplateData(app, w, r)
|
||||||
tmpl.ExecuteTemplate(w, "layout", context)
|
ctx := templateHelpers.TemplateContext(w, r, data)
|
||||||
|
ctx["Invites"] = invites
|
||||||
|
|
||||||
|
tmpl := templateHelpers.LoadTemplateFiles("invites.html", "web/templates/syndicate/invites.html")
|
||||||
|
_ = tmpl.ExecuteTemplate(w, "layout", ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AcceptInviteHandler(db *sql.DB) http.HandlerFunc {
|
// POST /syndicate/invites/accept?id=<invite_id>
|
||||||
|
func AcceptInviteHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
inviteID := helpers.Atoi(r.URL.Query().Get("id"))
|
inviteID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
templateHelpers.RenderError(w, r, 403)
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := storage.AcceptInvite(db, inviteID, userID)
|
if err := syndicateStorage.AcceptInvite(app.DB, inviteID, userID); err != nil {
|
||||||
if err != nil {
|
templateHelpers.SetFlash(r, "Failed to accept invite")
|
||||||
templateHelpers.SetFlash(w, r, "Failed to accept invite")
|
|
||||||
} else {
|
} else {
|
||||||
templateHelpers.SetFlash(w, r, "You have joined the syndicate")
|
templateHelpers.SetFlash(r, "You have joined the syndicate")
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
|
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeclineInviteHandler(db *sql.DB) http.HandlerFunc {
|
// POST /syndicate/invites/decline?id=<invite_id>
|
||||||
|
func DeclineInviteHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
inviteID := helpers.Atoi(r.URL.Query().Get("id"))
|
inviteID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||||
_ = storage.UpdateInviteStatus(db, inviteID, "declined")
|
_ = syndicateStorage.UpdateInviteStatus(app.DB, inviteID, "declined")
|
||||||
http.Redirect(w, r, "/syndicate/invites", http.StatusSeeOther)
|
http.Redirect(w, r, "/syndicate/invites", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateInviteToken(db *sql.DB, syndicateID, invitedByID int, ttlHours int) (string, error) {
|
// ===== Invite Tokens ========================================================
|
||||||
|
// (Consider moving these two helpers to internal/storage/syndicate)
|
||||||
|
|
||||||
|
// Create an invite token that expires after ttlHours.
|
||||||
|
func CreateInviteToken(app *bootstrap.App, syndicateID, invitedByID int, ttlHours int) (string, error) {
|
||||||
token, err := securityHelpers.GenerateSecureToken()
|
token, err := securityHelpers.GenerateSecureToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
expires := time.Now().Add(time.Duration(ttlHours) * time.Hour)
|
expires := time.Now().Add(time.Duration(ttlHours) * time.Hour)
|
||||||
|
|
||||||
_, err = db.Exec(`
|
_, err = app.DB.Exec(`
|
||||||
INSERT INTO syndicate_invite_tokens (syndicate_id, token, invited_by_user_id, expires_at)
|
INSERT INTO syndicate_invite_tokens (syndicate_id, token, invited_by_user_id, expires_at)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
`, syndicateID, token, invitedByID, expires)
|
`, syndicateID, token, invitedByID, expires)
|
||||||
|
|
||||||
return token, err
|
return token, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func AcceptInviteToken(db *sql.DB, token string, userID int) error {
|
// Validate + consume a token to join a syndicate.
|
||||||
|
func AcceptInviteToken(app *bootstrap.App, token string, userID int) error {
|
||||||
var syndicateID int
|
var syndicateID int
|
||||||
var expiresAt, acceptedAt sql.NullTime
|
var expiresAt, acceptedAt struct {
|
||||||
err := db.QueryRow(`
|
Valid bool
|
||||||
|
Time time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: using separate variables to avoid importing database/sql here.
|
||||||
|
row := app.DB.QueryRow(`
|
||||||
SELECT syndicate_id, expires_at, accepted_at
|
SELECT syndicate_id, expires_at, accepted_at
|
||||||
FROM syndicate_invite_tokens
|
FROM syndicate_invite_tokens
|
||||||
WHERE token = ?
|
WHERE token = ?
|
||||||
`, token).Scan(&syndicateID, &expiresAt, &acceptedAt)
|
`, token)
|
||||||
if err != nil {
|
if err := row.Scan(&syndicateID, &expiresAt.Time, &acceptedAt.Time); err != nil {
|
||||||
return fmt.Errorf("invalid or expired token")
|
return fmt.Errorf("invalid or expired token")
|
||||||
}
|
}
|
||||||
if acceptedAt.Valid || expiresAt.Time.Before(time.Now()) {
|
// If driver returns zero time when NULL, treat missing as invalid.Valid=false
|
||||||
|
expiresAt.Valid = !expiresAt.Time.IsZero()
|
||||||
|
acceptedAt.Valid = !acceptedAt.Time.IsZero()
|
||||||
|
|
||||||
|
if acceptedAt.Valid || (expiresAt.Valid && expiresAt.Time.Before(time.Now())) {
|
||||||
return fmt.Errorf("token already used or expired")
|
return fmt.Errorf("token already used or expired")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.Exec(`
|
if _, err := app.DB.Exec(`
|
||||||
INSERT INTO syndicate_members (syndicate_id, user_id, role, status, joined_at)
|
INSERT INTO syndicate_members (syndicate_id, user_id, role, status, joined_at)
|
||||||
VALUES (?, ?, 'member', 'active', CURRENT_TIMESTAMP)
|
VALUES (?, ?, 'member', 'active', CURRENT_TIMESTAMP)
|
||||||
`, syndicateID, userID)
|
`, syndicateID, userID); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.Exec(`
|
_, err := app.DB.Exec(`
|
||||||
UPDATE syndicate_invite_tokens
|
UPDATE syndicate_invite_tokens
|
||||||
SET accepted_by_user_id = ?, accepted_at = CURRENT_TIMESTAMP
|
SET accepted_by_user_id = ?, accepted_at = CURRENT_TIMESTAMP
|
||||||
WHERE token = ?
|
WHERE token = ?
|
||||||
@@ -144,38 +159,38 @@ func AcceptInviteToken(db *sql.DB, token string, userID int) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateInviteLinkHandler(db *sql.DB) http.HandlerFunc {
|
// GET /syndicate/invite/token?id=<syndicate_id>
|
||||||
|
func GenerateInviteLinkHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||||
token, err := CreateInviteToken(db, syndicateID, userID, 48)
|
token, err := CreateInviteToken(app, syndicateID, userID, 48)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
templateHelpers.SetFlash(w, r, "Failed to generate invite link.")
|
templateHelpers.SetFlash(r, "Failed to generate invite link.")
|
||||||
http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther)
|
http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
origin := r.Host
|
scheme := "http://"
|
||||||
if r.TLS != nil {
|
if r.TLS != nil {
|
||||||
origin = "https://" + origin
|
scheme = "https://"
|
||||||
} else {
|
|
||||||
origin = "http://" + origin
|
|
||||||
}
|
}
|
||||||
inviteLink := fmt.Sprintf("%s/syndicate/join?token=%s", origin, token)
|
inviteLink := fmt.Sprintf("%s%s/syndicate/join?token=%s", scheme, r.Host, token)
|
||||||
|
|
||||||
templateHelpers.SetFlash(w, r, "Invite link created: "+inviteLink)
|
templateHelpers.SetFlash(r, "Invite link created: "+inviteLink)
|
||||||
http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther)
|
http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func JoinSyndicateWithTokenHandler(db *sql.DB) http.HandlerFunc {
|
// GET /syndicate/join?token=<token>
|
||||||
|
func JoinSyndicateWithTokenHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
@@ -183,44 +198,43 @@ func JoinSyndicateWithTokenHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
|
|
||||||
token := r.URL.Query().Get("token")
|
token := r.URL.Query().Get("token")
|
||||||
if token == "" {
|
if token == "" {
|
||||||
templateHelpers.SetFlash(w, r, "Invalid or missing invite token.")
|
templateHelpers.SetFlash(r, "Invalid or missing invite token.")
|
||||||
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
|
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := AcceptInviteToken(db, token, userID)
|
if err := AcceptInviteToken(app, token, userID); err != nil {
|
||||||
if err != nil {
|
templateHelpers.SetFlash(r, "Failed to join syndicate: "+err.Error())
|
||||||
templateHelpers.SetFlash(w, r, "Failed to join syndicate: "+err.Error())
|
|
||||||
} else {
|
} else {
|
||||||
templateHelpers.SetFlash(w, r, "You have joined the syndicate!")
|
templateHelpers.SetFlash(r, "You have joined the syndicate!")
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
|
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ManageInviteTokensHandler(db *sql.DB) http.HandlerFunc {
|
// GET /syndicate/invite/tokens?id=<syndicate_id>
|
||||||
|
func ManageInviteTokensHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
templateHelpers.RenderError(w, r, 403)
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||||
|
if !syndicateStorage.IsSyndicateManager(app.DB, syndicateID, userID) {
|
||||||
if !storage.IsSyndicateManager(db, syndicateID, userID) {
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
templateHelpers.RenderError(w, r, 403)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens := storage.GetInviteTokensForSyndicate(db, syndicateID)
|
tokens := syndicateStorage.GetInviteTokensForSyndicate(app.DB, syndicateID)
|
||||||
|
|
||||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
data := templateHandlers.BuildTemplateData(app, w, r)
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
ctx := templateHelpers.TemplateContext(w, r, data)
|
||||||
context["Tokens"] = tokens
|
ctx["Tokens"] = tokens
|
||||||
context["SyndicateID"] = syndicateID
|
ctx["SyndicateID"] = syndicateID
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("invite-links.html", "templates/syndicate/invite_links.html")
|
tmpl := templateHelpers.LoadTemplateFiles("invite-links.html", "web/templates/syndicate/invite_links.html")
|
||||||
tmpl.ExecuteTemplate(w, "layout", context)
|
_ = tmpl.ExecuteTemplate(w, "layout", ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// internal/handlers/lottery/tickets/ticket_handler.go
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -10,21 +11,23 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
httpHelpers "synlotto-website/internal/helpers/http"
|
templateHandlers "synlotto-website/internal/handlers/template"
|
||||||
securityHelpers "synlotto-website/internal/helpers/security"
|
securityHelpers "synlotto-website/internal/helpers/security"
|
||||||
templateHelpers "synlotto-website/internal/helpers/template"
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
draws "synlotto-website/internal/services/draws"
|
draws "synlotto-website/internal/services/draws"
|
||||||
|
|
||||||
"synlotto-website/internal/helpers"
|
"synlotto-website/internal/helpers"
|
||||||
"synlotto-website/internal/models"
|
"synlotto-website/internal/models"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
|
||||||
"github.com/gorilla/csrf"
|
"github.com/justinas/nosurf"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AddTicket(db *sql.DB) http.HandlerFunc {
|
// AddTicket renders the add-ticket form (GET) and handles multi-line ticket submission (POST).
|
||||||
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
func AddTicket(app *bootstrap.App) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
rows, err := db.Query(`
|
rows, err := app.DB.Query(`
|
||||||
SELECT DISTINCT draw_date
|
SELECT DISTINCT draw_date
|
||||||
FROM results_thunderball
|
FROM results_thunderball
|
||||||
ORDER BY draw_date DESC
|
ORDER BY draw_date DESC
|
||||||
@@ -44,29 +47,27 @@ func AddTicket(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data := models.TemplateData{}
|
// Use shared template data builder (expects *bootstrap.App)
|
||||||
|
data := templateHandlers.BuildTemplateData(app, w, r)
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
context := templateHelpers.TemplateContext(w, r, data)
|
||||||
context["csrfField"] = csrf.TemplateField(r)
|
context["CSRFToken"] = nosurf.Token(r)
|
||||||
context["DrawDates"] = drawDates
|
context["DrawDates"] = drawDates
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("add_ticket.html", "templates/account/tickets/add_ticket.html")
|
tmpl := templateHelpers.LoadTemplateFiles("add_ticket.html", "web/templates/account/tickets/add_ticket.html")
|
||||||
|
if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil {
|
||||||
err = tmpl.ExecuteTemplate(w, "layout", context)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("❌ Template render error:", err)
|
log.Println("❌ Template render error:", err)
|
||||||
http.Error(w, "Error rendering form", http.StatusInternalServerError)
|
http.Error(w, "Error rendering form", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := r.ParseMultipartForm(10 << 20)
|
if err := r.ParseMultipartForm(10 << 20); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Invalid form", http.StatusBadRequest)
|
http.Error(w, "Invalid form", http.StatusBadRequest)
|
||||||
log.Println("❌ Failed to parse form:", err)
|
log.Println("❌ Failed to parse form:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
@@ -77,7 +78,6 @@ func AddTicket(db *sql.DB) http.HandlerFunc {
|
|||||||
purchaseMethod := r.FormValue("purchase_method")
|
purchaseMethod := r.FormValue("purchase_method")
|
||||||
purchaseDate := r.FormValue("purchase_date")
|
purchaseDate := r.FormValue("purchase_date")
|
||||||
purchaseTime := r.FormValue("purchase_time")
|
purchaseTime := r.FormValue("purchase_time")
|
||||||
|
|
||||||
if purchaseTime != "" {
|
if purchaseTime != "" {
|
||||||
purchaseDate += "T" + purchaseTime
|
purchaseDate += "T" + purchaseTime
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ func AddTicket(db *sql.DB) http.HandlerFunc {
|
|||||||
out, err := os.Create(filename)
|
out, err := os.Create(filename)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer out.Close()
|
defer out.Close()
|
||||||
io.Copy(out, file)
|
_, _ = io.Copy(out, file)
|
||||||
imagePath = filename
|
imagePath = filename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ func AddTicket(db *sql.DB) http.HandlerFunc {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := db.Exec(`
|
if _, err := app.DB.Exec(`
|
||||||
INSERT INTO my_tickets (
|
INSERT INTO my_tickets (
|
||||||
userId, game_type, draw_date,
|
userId, game_type, draw_date,
|
||||||
ball1, ball2, ball3, ball4, ball5, ball6,
|
ball1, ball2, ball3, ball4, ball5, ball6,
|
||||||
@@ -169,27 +169,26 @@ func AddTicket(db *sql.DB) http.HandlerFunc {
|
|||||||
b[0], b[1], b[2], b[3], b[4], b[5],
|
b[0], b[1], b[2], b[3], b[4], b[5],
|
||||||
bo[0], bo[1],
|
bo[0], bo[1],
|
||||||
purchaseMethod, purchaseDate, imagePath,
|
purchaseMethod, purchaseDate, imagePath,
|
||||||
)
|
); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Println("❌ Failed to insert ticket line:", err)
|
log.Println("❌ Failed to insert ticket line:", err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("✅ Ticket line %d saved", i+1) // ToDo create audit
|
log.Printf("✅ Ticket line %d saved", i+1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, "/tickets", http.StatusSeeOther)
|
http.Redirect(w, r, "/tickets", http.StatusSeeOther)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SubmitTicket(db *sql.DB) http.HandlerFunc {
|
// SubmitTicket handles alternate multipart ticket submission (POST-only).
|
||||||
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
func SubmitTicket(app *bootstrap.App) http.HandlerFunc {
|
||||||
err := r.ParseMultipartForm(10 << 20)
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if err != nil {
|
if err := r.ParseMultipartForm(10 << 20); err != nil {
|
||||||
http.Error(w, "Invalid form", http.StatusBadRequest)
|
http.Error(w, "Invalid form", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
@@ -200,7 +199,6 @@ func SubmitTicket(db *sql.DB) http.HandlerFunc {
|
|||||||
purchaseMethod := r.FormValue("purchase_method")
|
purchaseMethod := r.FormValue("purchase_method")
|
||||||
purchaseDate := r.FormValue("purchase_date")
|
purchaseDate := r.FormValue("purchase_date")
|
||||||
purchaseTime := r.FormValue("purchase_time")
|
purchaseTime := r.FormValue("purchase_time")
|
||||||
|
|
||||||
if purchaseTime != "" {
|
if purchaseTime != "" {
|
||||||
purchaseDate += "T" + purchaseTime
|
purchaseDate += "T" + purchaseTime
|
||||||
}
|
}
|
||||||
@@ -213,13 +211,13 @@ func SubmitTicket(db *sql.DB) http.HandlerFunc {
|
|||||||
out, err := os.Create(filename)
|
out, err := os.Create(filename)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer out.Close()
|
defer out.Close()
|
||||||
io.Copy(out, file)
|
_, _ = io.Copy(out, file)
|
||||||
imagePath = filename
|
imagePath = filename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ballCount := 6
|
const ballCount = 6
|
||||||
bonusCount := 2
|
const bonusCount = 2
|
||||||
|
|
||||||
balls := make([][]int, ballCount)
|
balls := make([][]int, ballCount)
|
||||||
bonuses := make([][]int, bonusCount)
|
bonuses := make([][]int, bonusCount)
|
||||||
@@ -247,7 +245,7 @@ func SubmitTicket(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := db.Exec(`
|
if _, err := app.DB.Exec(`
|
||||||
INSERT INTO my_tickets (
|
INSERT INTO my_tickets (
|
||||||
user_id, game_type, draw_date,
|
user_id, game_type, draw_date,
|
||||||
ball1, ball2, ball3, ball4, ball5, ball6,
|
ball1, ball2, ball3, ball4, ball5, ball6,
|
||||||
@@ -259,30 +257,30 @@ func SubmitTicket(db *sql.DB) http.HandlerFunc {
|
|||||||
b[0], b[1], b[2], b[3], b[4], b[5],
|
b[0], b[1], b[2], b[3], b[4], b[5],
|
||||||
bo[0], bo[1],
|
bo[0], bo[1],
|
||||||
purchaseMethod, purchaseDate, imagePath,
|
purchaseMethod, purchaseDate, imagePath,
|
||||||
)
|
); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Println("❌ Insert failed:", err)
|
log.Println("❌ Insert failed:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, "/tickets", http.StatusSeeOther)
|
http.Redirect(w, r, "/tickets", http.StatusSeeOther)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetMyTickets(db *sql.DB) http.HandlerFunc {
|
// GetMyTickets lists the current user's tickets.
|
||||||
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
func GetMyTickets(app *bootstrap.App) http.HandlerFunc {
|
||||||
data := models.TemplateData{}
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var tickets []models.Ticket
|
// Use shared template data builder (ensures user/flash/notifications present)
|
||||||
|
data := templateHandlers.BuildTemplateData(app, w, r)
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
context := templateHelpers.TemplateContext(w, r, data)
|
||||||
context["Tickets"] = tickets
|
|
||||||
|
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := db.Query(`
|
var tickets []models.Ticket
|
||||||
|
rows, err := app.DB.Query(`
|
||||||
SELECT id, game_type, draw_date,
|
SELECT id, game_type, draw_date,
|
||||||
ball1, ball2, ball3, ball4, ball5, ball6,
|
ball1, ball2, ball3, ball4, ball5, ball6,
|
||||||
bonus1, bonus2,
|
bonus1, bonus2,
|
||||||
@@ -308,19 +306,18 @@ func GetMyTickets(db *sql.DB) http.HandlerFunc {
|
|||||||
var prizeLabel sql.NullString
|
var prizeLabel sql.NullString
|
||||||
var prizeAmount sql.NullFloat64
|
var prizeAmount sql.NullFloat64
|
||||||
|
|
||||||
err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&t.Id, &t.GameType, &t.DrawDate,
|
&t.Id, &t.GameType, &t.DrawDate,
|
||||||
&b1, &b2, &b3, &b4, &b5, &b6,
|
&b1, &b2, &b3, &b4, &b5, &b6,
|
||||||
&bo1, &bo2,
|
&bo1, &bo2,
|
||||||
&t.PurchaseMethod, &t.PurchaseDate, &t.ImagePath, &t.Duplicate,
|
&t.PurchaseMethod, &t.PurchaseDate, &t.ImagePath, &t.Duplicate,
|
||||||
&matchedMain, &matchedBonus, &prizeTier, &isWinner, &prizeLabel, &prizeAmount,
|
&matchedMain, &matchedBonus, &prizeTier, &isWinner, &prizeLabel, &prizeAmount,
|
||||||
)
|
); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Println("⚠️ Failed to scan ticket row:", err)
|
log.Println("⚠️ Failed to scan ticket row:", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build primary number + bonus fields
|
// Normalize fields
|
||||||
t.Ball1 = int(b1.Int64)
|
t.Ball1 = int(b1.Int64)
|
||||||
t.Ball2 = int(b2.Int64)
|
t.Ball2 = int(b2.Int64)
|
||||||
t.Ball3 = int(b3.Int64)
|
t.Ball3 = int(b3.Int64)
|
||||||
@@ -348,28 +345,55 @@ func GetMyTickets(db *sql.DB) http.HandlerFunc {
|
|||||||
if prizeAmount.Valid {
|
if prizeAmount.Valid {
|
||||||
t.PrizeAmount = prizeAmount.Float64
|
t.PrizeAmount = prizeAmount.Float64
|
||||||
}
|
}
|
||||||
// Build balls slices (for template use)
|
|
||||||
|
// Derived fields for templates
|
||||||
t.Balls = helpers.BuildBallsSlice(t)
|
t.Balls = helpers.BuildBallsSlice(t)
|
||||||
t.BonusBalls = helpers.BuildBonusSlice(t)
|
t.BonusBalls = helpers.BuildBonusSlice(t)
|
||||||
|
|
||||||
// 🎯 Get the actual draw info (used to show which numbers matched)
|
// Fetch matching draw info
|
||||||
draw := draws.GetDrawResultForTicket(db, t.GameType, t.DrawDate)
|
draw := draws.GetDrawResultForTicket(app.DB, t.GameType, t.DrawDate)
|
||||||
t.MatchedDraw = draw
|
t.MatchedDraw = draw
|
||||||
|
|
||||||
// ✅ DEBUG
|
|
||||||
log.Printf("✅ Ticket #%d", t.Id)
|
|
||||||
log.Printf("Balls: %v", t.Balls)
|
|
||||||
log.Printf("DrawResult: %+v", draw)
|
|
||||||
|
|
||||||
tickets = append(tickets, t)
|
tickets = append(tickets, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("my_tickets.html", "templates/account/tickets/my_tickets.html")
|
context["Tickets"] = tickets
|
||||||
|
|
||||||
err = tmpl.ExecuteTemplate(w, "layout", context)
|
tmpl := templateHelpers.LoadTemplateFiles("my_tickets.html", "web/templates/account/tickets/my_tickets.html")
|
||||||
if err != nil {
|
if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil {
|
||||||
log.Println("❌ Template error:", err)
|
log.Println("❌ Template error:", err)
|
||||||
http.Error(w, "Error rendering page", http.StatusInternalServerError)
|
http.Error(w, "Error rendering page", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToDo
|
||||||
|
// http: superfluous response.WriteHeader call (from SCS)
|
||||||
|
|
||||||
|
//This happens when headers are written twice in a request. With SCS, it sets cookies in WriteHeader. If something else already wrote the headers (or wrote them again), you see this warning.
|
||||||
|
|
||||||
|
//Common culprits & fixes:
|
||||||
|
|
||||||
|
//Use Gin’s redirect instead of the stdlib one:
|
||||||
|
|
||||||
|
// Replace:
|
||||||
|
//http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
||||||
|
|
||||||
|
// With:
|
||||||
|
//c.Redirect(http.StatusSeeOther, "/account/login")
|
||||||
|
//c.Abort() // stop further handlers writing
|
||||||
|
|
||||||
|
//Do this everywhere you redirect (signup, login, logout).
|
||||||
|
|
||||||
|
//Don’t call two status-writes. For template GETs, this is fine:
|
||||||
|
|
||||||
|
//c.Status(http.StatusOK)
|
||||||
|
//_ = tmpl.ExecuteTemplate(c.Writer, "layout", ctx) // writes body once
|
||||||
|
|
||||||
|
//Just make sure you never write another header after that.
|
||||||
|
|
||||||
|
//Keep your wrapping order as you have it (it’s correct):
|
||||||
|
|
||||||
|
//Gin → SCS.LoadAndSave → NoSurf → http.Server
|
||||||
|
|
||||||
|
//If you still get the warning after switching to c.Redirect + c.Abort(), tell me which handler it’s coming from and I’ll point to the exact double-write.
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
templateHandlers "synlotto-website/internal/handlers/template"
|
templateHandlers "synlotto-website/internal/handlers/template"
|
||||||
httpHelpers "synlotto-website/internal/helpers/http"
|
|
||||||
securityHelpers "synlotto-website/internal/helpers/security"
|
securityHelpers "synlotto-website/internal/helpers/security"
|
||||||
templateHelpers "synlotto-website/internal/helpers/template"
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
messagesStorage "synlotto-website/internal/storage/messages"
|
||||||
|
|
||||||
"synlotto-website/internal/helpers"
|
"synlotto-website/internal/helpers"
|
||||||
storage "synlotto-website/internal/storage/mysql"
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MessagesInboxHandler(db *sql.DB) http.HandlerFunc {
|
// Inbox: paginated list of messages
|
||||||
|
func MessagesInboxHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
templateHelpers.RenderError(w, r, 403)
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,86 +28,82 @@ func MessagesInboxHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
perPage := 10
|
perPage := 10
|
||||||
|
|
||||||
totalCount := storage.GetInboxMessageCount(db, userID)
|
totalCount := messagesStorage.GetInboxMessageCount(app.DB, userID)
|
||||||
totalPages := (totalCount + perPage - 1) / perPage
|
totalPages := (totalCount + perPage - 1) / perPage
|
||||||
if totalPages == 0 {
|
if totalPages == 0 {
|
||||||
totalPages = 1
|
totalPages = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
messages := storage.GetInboxMessages(db, userID, page, perPage)
|
messages := messagesStorage.GetInboxMessages(app.DB, userID, page, perPage)
|
||||||
|
|
||||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
data := templateHandlers.BuildTemplateData(app, w, r)
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
ctx := templateHelpers.TemplateContext(w, r, data)
|
||||||
|
ctx["Messages"] = messages
|
||||||
|
ctx["CurrentPage"] = page
|
||||||
|
ctx["TotalPages"] = totalPages
|
||||||
|
ctx["PageRange"] = templateHelpers.PageRange(page, totalPages)
|
||||||
|
|
||||||
context["Messages"] = messages
|
tmpl := templateHelpers.LoadTemplateFiles("messages.html", "web/templates/account/messages/index.html")
|
||||||
context["CurrentPage"] = page
|
if err := tmpl.ExecuteTemplate(w, "layout", ctx); err != nil {
|
||||||
context["TotalPages"] = totalPages
|
templateHelpers.RenderError(w, r, http.StatusInternalServerError)
|
||||||
context["PageRange"] = templateHelpers.PageRange(page, totalPages)
|
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("messages.html", "templates/account/messages/index.html")
|
|
||||||
|
|
||||||
if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil {
|
|
||||||
// ToDo: Make this load all error pages without defining explictly.
|
|
||||||
templateHelpers.RenderError(w, r, 500)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadMessageHandler(db *sql.DB) http.HandlerFunc {
|
// Read a single message (marks as read)
|
||||||
|
func ReadMessageHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
idStr := r.URL.Query().Get("id")
|
id := helpers.Atoi(r.URL.Query().Get("id"))
|
||||||
messageID := helpers.Atoi(idStr)
|
|
||||||
|
|
||||||
session, _ := httpHelpers.GetSession(w, r)
|
userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
userID, ok := session.Values["user_id"].(int)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
templateHelpers.RenderError(w, r, 403)
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
message, err := storage.GetMessageByID(db, userID, messageID)
|
message, err := messagesStorage.GetMessageByID(app.DB, userID, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("❌ Message not found: %v", err)
|
log.Printf("❌ Message not found: %v", err)
|
||||||
message = nil
|
message = nil
|
||||||
} else if !message.IsRead {
|
} else if message != nil && !message.IsRead {
|
||||||
_ = storage.MarkMessageAsRead(db, messageID, userID)
|
_ = messagesStorage.MarkMessageAsRead(app.DB, id, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
data := templateHandlers.BuildTemplateData(app, w, r)
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
ctx := templateHelpers.TemplateContext(w, r, data)
|
||||||
context["Message"] = message
|
ctx["Message"] = message
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("read-message.html", "templates/account/messages/read.html")
|
tmpl := templateHelpers.LoadTemplateFiles("read-message.html", "web/templates/account/messages/read.html")
|
||||||
|
_ = tmpl.ExecuteTemplate(w, "layout", ctx)
|
||||||
tmpl.ExecuteTemplate(w, "layout", context)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ArchiveMessageHandler(db *sql.DB) http.HandlerFunc {
|
// Archive a message
|
||||||
|
func ArchiveMessageHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
id := helpers.Atoi(r.URL.Query().Get("id"))
|
id := helpers.Atoi(r.URL.Query().Get("id"))
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
templateHelpers.RenderError(w, r, 403)
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := storage.ArchiveMessage(db, userID, id)
|
if err := messagesStorage.ArchiveMessage(app.DB, userID, id); err != nil {
|
||||||
if err != nil {
|
templateHelpers.SetFlash(r, "Failed to archive message.")
|
||||||
templateHelpers.SetFlash(w, r, "Failed to archive message.")
|
|
||||||
} else {
|
} else {
|
||||||
templateHelpers.SetFlash(w, r, "Message archived.")
|
templateHelpers.SetFlash(r, "Message archived.")
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, "/account/messages", http.StatusSeeOther)
|
http.Redirect(w, r, "/account/messages", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ArchivedMessagesHandler(db *sql.DB) http.HandlerFunc {
|
// List archived messages (paged)
|
||||||
|
func ArchivedMessagesHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
templateHelpers.RenderError(w, r, 403)
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,35 +113,35 @@ func ArchivedMessagesHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
perPage := 10
|
perPage := 10
|
||||||
|
|
||||||
messages := storage.GetArchivedMessages(db, userID, page, perPage)
|
messages := messagesStorage.GetArchivedMessages(app.DB, userID, page, perPage)
|
||||||
hasMore := len(messages) == perPage
|
hasMore := len(messages) == perPage
|
||||||
|
|
||||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
data := templateHandlers.BuildTemplateData(app, w, r)
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
ctx := templateHelpers.TemplateContext(w, r, data)
|
||||||
context["Messages"] = messages
|
ctx["Messages"] = messages
|
||||||
context["Page"] = page
|
ctx["Page"] = page
|
||||||
context["HasMore"] = hasMore
|
ctx["HasMore"] = hasMore
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("archived.html", "templates/account/messages/archived.html")
|
tmpl := templateHelpers.LoadTemplateFiles("archived.html", "web/templates/account/messages/archived.html")
|
||||||
tmpl.ExecuteTemplate(w, "layout", context)
|
_ = tmpl.ExecuteTemplate(w, "layout", ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendMessageHandler(db *sql.DB) http.HandlerFunc {
|
// Compose & send message
|
||||||
|
func SendMessageHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
data := templateHandlers.BuildTemplateData(app, w, r)
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
ctx := templateHelpers.TemplateContext(w, r, data)
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("send-message.html", "templates/account/messages/send.html")
|
tmpl := templateHelpers.LoadTemplateFiles("send-message.html", "web/templates/account/messages/send.html")
|
||||||
|
if err := tmpl.ExecuteTemplate(w, "layout", ctx); err != nil {
|
||||||
if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil {
|
templateHelpers.RenderError(w, r, http.StatusInternalServerError)
|
||||||
templateHelpers.RenderError(w, r, 500)
|
|
||||||
}
|
}
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
senderID, ok := securityHelpers.GetCurrentUserID(r)
|
senderID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
templateHelpers.RenderError(w, r, 403)
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,32 +149,32 @@ func SendMessageHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
subject := r.FormValue("subject")
|
subject := r.FormValue("subject")
|
||||||
body := r.FormValue("message")
|
body := r.FormValue("message")
|
||||||
|
|
||||||
if err := storage.SendMessage(db, senderID, recipientID, subject, body); err != nil {
|
if err := messagesStorage.SendMessage(app.DB, senderID, recipientID, subject, body); err != nil {
|
||||||
templateHelpers.SetFlash(w, r, "Failed to send message.")
|
templateHelpers.SetFlash(r, "Failed to send message.")
|
||||||
} else {
|
} else {
|
||||||
templateHelpers.SetFlash(w, r, "Message sent.")
|
templateHelpers.SetFlash(r, "Message sent.")
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, "/account/messages", http.StatusSeeOther)
|
http.Redirect(w, r, "/account/messages", http.StatusSeeOther)
|
||||||
default:
|
default:
|
||||||
templateHelpers.RenderError(w, r, 405)
|
templateHelpers.RenderError(w, r, http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func RestoreMessageHandler(db *sql.DB) http.HandlerFunc {
|
// Restore an archived message
|
||||||
|
func RestoreMessageHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
id := helpers.Atoi(r.URL.Query().Get("id"))
|
id := helpers.Atoi(r.URL.Query().Get("id"))
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
templateHelpers.RenderError(w, r, 403)
|
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := storage.RestoreMessage(db, userID, id)
|
if err := messagesStorage.RestoreMessage(app.DB, userID, id); err != nil {
|
||||||
if err != nil {
|
templateHelpers.SetFlash(r, "Failed to restore message.")
|
||||||
templateHelpers.SetFlash(w, r, "Failed to restore message.")
|
|
||||||
} else {
|
} else {
|
||||||
templateHelpers.SetFlash(w, r, "Message restored.")
|
templateHelpers.SetFlash(r, "Message restored.")
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, "/account/messages/archived", http.StatusSeeOther)
|
http.Redirect(w, r, "/account/messages/archived", http.StatusSeeOther)
|
||||||
|
|||||||
@@ -1,70 +1,73 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
templateHandlers "synlotto-website/internal/handlers/template"
|
templateHandlers "synlotto-website/internal/handlers/template"
|
||||||
httpHelpers "synlotto-website/internal/helpers/http"
|
|
||||||
templateHelpers "synlotto-website/internal/helpers/template"
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
"synlotto-website/internal/storage"
|
"synlotto-website/internal/platform/sessionkeys"
|
||||||
|
notificationsStorage "synlotto-website/internal/storage/notifications"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NotificationsHandler(db *sql.DB) http.HandlerFunc {
|
// NotificationsHandler serves the notifications index page.
|
||||||
|
// New signature: accept *bootstrap.App (not *sql.DB)
|
||||||
|
func NotificationsHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
data := templateHandlers.BuildTemplateData(app, w, r)
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
context := templateHelpers.TemplateContext(w, r, data)
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("index.html", "templates/account/notifications/index.html")
|
tmpl := templateHelpers.LoadTemplateFiles("index.html", "web/templates/account/notifications/index.html")
|
||||||
|
|
||||||
err := tmpl.ExecuteTemplate(w, "layout", context)
|
if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Println("❌ Template render error:", err)
|
log.Println("❌ Template render error:", err)
|
||||||
http.Error(w, "Error rendering notifications page", http.StatusInternalServerError)
|
http.Error(w, "Error rendering notifications page", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func MarkNotificationReadHandler(db *sql.DB) http.HandlerFunc {
|
// MarkNotificationReadHandler shows a single notification (and marks unread ones as read).
|
||||||
|
// New signature: accept *bootstrap.App; read user id from SCS session.
|
||||||
|
func MarkNotificationReadHandler(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
notificationIDStr := r.URL.Query().Get("id")
|
notificationIDStr := r.URL.Query().Get("id")
|
||||||
notificationID, err := strconv.Atoi(notificationIDStr)
|
notificationID, err := strconv.Atoi(notificationIDStr)
|
||||||
if err != nil {
|
if err != nil || notificationID <= 0 {
|
||||||
http.Error(w, "Invalid notification ID", http.StatusBadRequest)
|
http.Error(w, "Invalid notification ID", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
session, _ := httpHelpers.GetSession(w, r)
|
// SCS-native session access
|
||||||
userID, ok := session.Values["user_id"].(int)
|
userID := app.SessionManager.GetInt(r.Context(), sessionkeys.UserID)
|
||||||
if !ok {
|
if userID == 0 {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
notification, err := storage.GetNotificationByID(db, userID, notificationID)
|
// Load + mark-as-read (if needed)
|
||||||
|
notification, err := notificationsStorage.GetNotificationByID(app.DB, userID, notificationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("❌ Notification not found or belongs to another user: %v", err)
|
log.Printf("❌ Notification not found or belongs to another user: %v", err)
|
||||||
notification = nil
|
notification = nil
|
||||||
} else if !notification.IsRead {
|
} else if !notification.IsRead {
|
||||||
err = storage.MarkNotificationAsRead(db, userID, notificationID)
|
if err := notificationsStorage.MarkNotificationAsRead(app.DB, userID, notificationID); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Printf("⚠️ Failed to mark as read: %v", err)
|
log.Printf("⚠️ Failed to mark as read: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
data := templateHandlers.BuildTemplateData(app, w, r)
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
context := templateHelpers.TemplateContext(w, r, data)
|
||||||
context["Notification"] = notification
|
context["Notification"] = notification
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("read.html", "templates/account/notifications/read.html")
|
tmpl := templateHelpers.LoadTemplateFiles("read.html", "web/templates/account/notifications/read.html")
|
||||||
|
|
||||||
err = tmpl.ExecuteTemplate(w, "layout", context)
|
if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Printf("❌ Template render error: %v", err)
|
log.Printf("❌ Template render error: %v", err)
|
||||||
http.Error(w, "Template render error", http.StatusInternalServerError)
|
http.Error(w, "Template render error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ func ResultsThunderball(db *sql.DB) http.HandlerFunc {
|
|||||||
noResultsMsg = "No results found for \"" + query + "\""
|
noResultsMsg = "No results found for \"" + query + "\""
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("thunderball.html", "templates/results/thunderball.html")
|
tmpl := templateHelpers.LoadTemplateFiles("thunderball.html", "web/templates/results/thunderball.html")
|
||||||
|
|
||||||
err = tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{
|
err = tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{
|
||||||
"Results": results,
|
"Results": results,
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
SessionStore *sessions.CookieStore
|
|
||||||
Name string
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetSession(w http.ResponseWriter, r *http.Request) (*sessions.Session, error) {
|
|
||||||
if SessionStore == nil {
|
|
||||||
return nil, fmt.Errorf("session store not initialized")
|
|
||||||
}
|
|
||||||
if Name == "" {
|
|
||||||
return nil, fmt.Errorf("session name not configured")
|
|
||||||
}
|
|
||||||
return SessionStore.Get(r, Name)
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/securecookie"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
authKey []byte
|
|
||||||
encryptKey []byte
|
|
||||||
)
|
|
||||||
|
|
||||||
func SecureCookie(w http.ResponseWriter, name, value string, isProduction bool) error {
|
|
||||||
s := securecookie.New(authKey, encryptKey)
|
|
||||||
|
|
||||||
encoded, err := s.Encode(name, value)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: name,
|
|
||||||
Value: encoded,
|
|
||||||
Path: "/",
|
|
||||||
HttpOnly: true,
|
|
||||||
Secure: isProduction,
|
|
||||||
SameSite: http.SameSiteStrictMode,
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,36 +1,34 @@
|
|||||||
|
// internal/handlers/statistics/thunderball.go
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
templateHandlers "synlotto-website/internal/handlers/template"
|
templateHandlers "synlotto-website/internal/handlers/template"
|
||||||
templateHelpers "synlotto-website/internal/helpers/template"
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
|
||||||
"synlotto-website/internal/http/middleware"
|
"synlotto-website/internal/http/middleware"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func StatisticsThunderball(db *sql.DB) http.HandlerFunc {
|
func StatisticsThunderball(app *bootstrap.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||||
limiter := middleware.GetVisitorLimiter(ip)
|
limiter := middleware.GetVisitorLimiter(ip)
|
||||||
|
|
||||||
if !limiter.Allow() {
|
if !limiter.Allow() {
|
||||||
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
|
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data := templateHandlers.BuildTemplateData(db, w, r)
|
data := templateHandlers.BuildTemplateData(app, w, r)
|
||||||
context := templateHelpers.TemplateContext(w, r, data)
|
context := templateHelpers.TemplateContext(w, r, data)
|
||||||
|
|
||||||
tmpl := templateHelpers.LoadTemplateFiles("statistics.html", "templates/statistics/thunderball.html")
|
tmpl := templateHelpers.LoadTemplateFiles("statistics.html", "web/templates/statistics/thunderball.html")
|
||||||
|
if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil {
|
||||||
err := tmpl.ExecuteTemplate(w, "layout", context)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("❌ Template render error:", err)
|
log.Println("❌ Template render error:", err)
|
||||||
http.Error(w, "Error rendering homepage", http.StatusInternalServerError)
|
http.Error(w, "Error rendering Thunderball statistics page", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
56
internal/handlers/template/error.go
Normal file
56
internal/handlers/template/error.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// internal/handlers/template/error.go
|
||||||
|
package templateHandler
|
||||||
|
|
||||||
|
// ToDo not nessisarily an issue with this file but ✅ internal/handlers/template/
|
||||||
|
//→ For anything that handles HTTP rendering (RenderError, RenderPage)
|
||||||
|
|
||||||
|
//✅ internal/helpers/template/
|
||||||
|
//→ For anything that helps render (TemplateContext, pagination, funcs)
|
||||||
|
// there for bear usages between helpers and handlers
|
||||||
|
//In clean Go architecture (especially following “Package by responsibility”):
|
||||||
|
|
||||||
|
//Type Responsibility Should access
|
||||||
|
//Helpers / Utilities Pure, stateless logic — e.g. template functions, math, formatters. Shared logic, no config, no HTTP handlers.
|
||||||
|
//Handlers Own an HTTP concern — e.g. routes, rendering responses, returning templates or JSON. Injected dependencies (cfg, db, etc.). Should use helpers, not vice versa.
|
||||||
|
|
||||||
|
// ToDo: duplicated work of internal/http/error/errors.go?
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
|
||||||
|
"synlotto-website/internal/models"
|
||||||
|
|
||||||
|
"github.com/alexedwards/scs/v2"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RenderError(c *gin.Context, sessions *scs.SessionManager, status int) {
|
||||||
|
// Base context
|
||||||
|
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{})
|
||||||
|
|
||||||
|
// Flash
|
||||||
|
if f := sessions.PopString(c.Request.Context(), "flash"); f != "" {
|
||||||
|
ctx["Flash"] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Correct template paths
|
||||||
|
pagePath := fmt.Sprintf("web/templates/error/%d.html", status)
|
||||||
|
if _, err := os.Stat(pagePath); err != nil {
|
||||||
|
c.String(status, http.StatusText(status))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := templateHelpers.LoadTemplateFiles(
|
||||||
|
"web/templates/layout.html",
|
||||||
|
pagePath,
|
||||||
|
)
|
||||||
|
|
||||||
|
c.Status(status)
|
||||||
|
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil {
|
||||||
|
c.String(status, http.StatusText(status))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
19
internal/handlers/template/render.go
Normal file
19
internal/handlers/template/render.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package templateHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"synlotto-website/internal/platform/config"
|
||||||
|
|
||||||
|
"github.com/alexedwards/scs/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
cfg config.Config
|
||||||
|
Sessions *scs.SessionManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.Config, sessions *scs.SessionManager) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
cfg: cfg,
|
||||||
|
Sessions: sessions,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +1,52 @@
|
|||||||
package handlers
|
// internal/handlers/template/templatedata.go
|
||||||
|
package templateHandler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
httpHelper "synlotto-website/internal/helpers/http"
|
messageStorage "synlotto-website/internal/storage/messages"
|
||||||
|
notificationStorage "synlotto-website/internal/storage/notifications"
|
||||||
|
usersStorage "synlotto-website/internal/storage/users"
|
||||||
|
|
||||||
"synlotto-website/internal/models"
|
"synlotto-website/internal/models"
|
||||||
"synlotto-website/internal/storage"
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
"synlotto-website/internal/platform/sessionkeys"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BuildTemplateData(db *sql.DB, w http.ResponseWriter, r *http.Request) models.TemplateData {
|
// BuildTemplateData aggregates common UI data (user, notifications, messages)
|
||||||
session, err := httpHelper.GetSession(w, r)
|
// from the current SCS session + DB.
|
||||||
if err != nil {
|
func BuildTemplateData(app *bootstrap.App, w http.ResponseWriter, r *http.Request) models.TemplateData {
|
||||||
log.Printf("Session error: %v", err)
|
sm := app.SessionManager
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
var (
|
||||||
|
user *models.User
|
||||||
|
isAdmin bool
|
||||||
|
notificationCount int
|
||||||
|
notifications []models.Notification
|
||||||
|
messageCount int
|
||||||
|
messages []models.Message
|
||||||
|
)
|
||||||
|
|
||||||
|
// Read user_id from SCS (may be int or int64 depending on writes)
|
||||||
|
if v := sm.Get(ctx, sessionkeys.UserID); v != nil {
|
||||||
|
var uid int64
|
||||||
|
switch t := v.(type) {
|
||||||
|
case int64:
|
||||||
|
uid = t
|
||||||
|
case int:
|
||||||
|
uid = int64(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
var user *models.User
|
if uid > 0 {
|
||||||
var isAdmin bool
|
if u := usersStorage.GetUserByID(app.DB, int(uid)); u != nil {
|
||||||
var notificationCount int
|
user = u
|
||||||
var notifications []models.Notification
|
isAdmin = u.IsAdmin
|
||||||
var messageCount int
|
notificationCount = notificationStorage.GetNotificationCount(app.DB, int(u.Id))
|
||||||
var messages []models.Message
|
notifications = notificationStorage.GetRecentNotifications(app.DB, int(u.Id), 15)
|
||||||
|
messageCount, _ = messageStorage.GetMessageCount(app.DB, int(u.Id))
|
||||||
if userId, ok := session.Values["user_id"].(int); ok {
|
messages = messageStorage.GetRecentMessages(app.DB, int(u.Id), 15)
|
||||||
user = storage.GetUserByID(db, userId)
|
}
|
||||||
if user != nil {
|
|
||||||
isAdmin = user.IsAdmin
|
|
||||||
notificationCount = storage.GetNotificationCount(db, user.Id)
|
|
||||||
notifications = storage.GetRecentNotifications(db, user.Id, 15)
|
|
||||||
messageCount, _ = storage.GetMessageCount(db, user.Id)
|
|
||||||
messages = storage.GetRecentMessages(db, user.Id, 15)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
68
internal/helpers/database/statements.go
Normal file
68
internal/helpers/database/statements.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package databaseHelpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"database/sql"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExecScript executes a multi-statement SQL script.
|
||||||
|
// It only requires that statements end with ';' and ignores '--' comments.
|
||||||
|
// (Good for simple DDL/DML. If you add routines/triggers, upgrade later.)
|
||||||
|
func ExecScript(tx *sql.Tx, script string) error {
|
||||||
|
sc := bufio.NewScanner(strings.NewReader(script))
|
||||||
|
sc.Split(splitStatements)
|
||||||
|
|
||||||
|
for sc.Scan() {
|
||||||
|
stmt := strings.TrimSpace(sc.Text())
|
||||||
|
if stmt == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(stmt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sc.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitStatements separates statements at ';'
|
||||||
|
// and strips whitespace and '--' comments.
|
||||||
|
func splitStatements(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
|
// skip whitespace and comments
|
||||||
|
start := 0
|
||||||
|
for {
|
||||||
|
// whitespace
|
||||||
|
for start < len(data) {
|
||||||
|
switch data[start] {
|
||||||
|
case ' ', '\t', '\n', '\r':
|
||||||
|
start++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// '-- comment'
|
||||||
|
if start+1 < len(data) && data[start] == '-' && data[start+1] == '-' {
|
||||||
|
i := start + 2
|
||||||
|
for i < len(data) && data[i] != '\n' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if i >= len(data) {
|
||||||
|
return len(data), nil, nil
|
||||||
|
}
|
||||||
|
start = i + 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// detect semicolon termination
|
||||||
|
for i := start; i < len(data); i++ {
|
||||||
|
if data[i] == ';' {
|
||||||
|
return i + 1, data[start:i], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if atEOF && start < len(data) {
|
||||||
|
return len(data), data[start:], nil
|
||||||
|
}
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
19
internal/helpers/http/request.go
Normal file
19
internal/helpers/http/request.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package httpHelpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ClientIP(r *http.Request) string {
|
||||||
|
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||||
|
parts := strings.Split(xff, ",")
|
||||||
|
return strings.TrimSpace(parts[0])
|
||||||
|
}
|
||||||
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
return r.RemoteAddr
|
||||||
|
}
|
||||||
|
return host
|
||||||
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package helpers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
session "synlotto-website/internal/handlers/session"
|
|
||||||
|
|
||||||
"synlotto-website/internal/platform/constants"
|
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetSession(w http.ResponseWriter, r *http.Request) (*sessions.Session, error) {
|
|
||||||
return session.GetSession(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsSessionExpired(session *sessions.Session) bool {
|
|
||||||
last, ok := session.Values["last_activity"].(time.Time)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return time.Since(last) > constants.SessionDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateSessionActivity(session *sessions.Session, r *http.Request, w http.ResponseWriter) {
|
|
||||||
session.Values["last_activity"] = time.Now().UTC()
|
|
||||||
session.Save(r, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
session, _ := GetSession(w, r)
|
|
||||||
|
|
||||||
if IsSessionExpired(session) {
|
|
||||||
session.Options.MaxAge = -1
|
|
||||||
session.Save(r, w)
|
|
||||||
|
|
||||||
newSession, _ := GetSession(w, r)
|
|
||||||
newSession.Values["flash"] = "Your session has timed out."
|
|
||||||
newSession.Save(r, w)
|
|
||||||
|
|
||||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateSessionActivity(session, r, w)
|
|
||||||
|
|
||||||
next(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,15 +3,12 @@ package security
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
httpHelpers "synlotto-website/internal/helpers/http"
|
"synlotto-website/internal/platform/sessionkeys"
|
||||||
|
|
||||||
|
"github.com/alexedwards/scs/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetCurrentUserID(r *http.Request) (int, bool) {
|
func GetCurrentUserID(sm *scs.SessionManager, r *http.Request) (int, bool) {
|
||||||
session, err := httpHelpers.GetSession(nil, r)
|
userID := sm.GetInt(r.Context(), sessionkeys.UserID)
|
||||||
if err != nil {
|
return userID, userID != 0
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
id, ok := session.Values["user_id"].(int)
|
|
||||||
return id, ok
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
package helpers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func LoadKeyFromFile(path string) ([]byte, error) {
|
|
||||||
key, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return bytes.TrimSpace(key), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ZeroBytes(b []byte) {
|
|
||||||
for i := range b {
|
|
||||||
b[i] = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
69
internal/helpers/session/remember.go
Normal file
69
internal/helpers/session/remember.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/base64"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func randomBase64(n int) (string, error) {
|
||||||
|
b := make([]byte, n)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func HashVerifier(verifier string) string {
|
||||||
|
sum := sha256.Sum256([]byte(verifier))
|
||||||
|
return base64.RawURLEncoding.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreToken inserts a new token row
|
||||||
|
func StoreToken(db *sql.DB, userID int64, selector, verifierHash string, expiresAt time.Time) error {
|
||||||
|
_, err := db.Exec(`
|
||||||
|
INSERT INTO remember_tokens (user_id, selector, verifier_hash, issued_at, expires_at)
|
||||||
|
VALUES ($1,$2,$3,NOW(),$4)`, userID, selector, verifierHash, expiresAt)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindToken fetches selector+hash
|
||||||
|
func FindToken(db *sql.DB, selector string) (userID int64, verifierHash string, expiresAt time.Time, revokedAt *time.Time, err error) {
|
||||||
|
err = db.QueryRow(`SELECT user_id, verifier_hash, expires_at, revoked_at FROM remember_tokens WHERE selector=$1`, selector).
|
||||||
|
Scan(&userID, &verifierHash, &expiresAt, &revokedAt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokeToken marks token as revoked
|
||||||
|
func RevokeToken(db *sql.DB, selector string) error {
|
||||||
|
_, err := db.Exec(`UPDATE remember_tokens SET revoked_at=NOW() WHERE selector=$1`, selector)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateAndStore creates a new remember-me token, stores it server-side,
|
||||||
|
// and returns the cookie-safe plaintext value to set on the client
|
||||||
|
func GenerateAndStore(db *sql.DB, userID int64, duration time.Duration) (string, time.Time, error) {
|
||||||
|
selector, err := randomBase64(16)
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
verifier, err := randomBase64(32)
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := HashVerifier(verifier)
|
||||||
|
expires := time.Now().Add(duration)
|
||||||
|
|
||||||
|
if err := StoreToken(db, userID, selector, hash, expires); err != nil {
|
||||||
|
return "", time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// The client cookie value contains selector + verifier
|
||||||
|
cookieVal := selector + ":" + verifier
|
||||||
|
|
||||||
|
return cookieVal, expires, nil
|
||||||
|
}
|
||||||
@@ -1,44 +1,49 @@
|
|||||||
package helpers
|
package templateHelper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
helpers "synlotto-website/internal/helpers/http"
|
|
||||||
"synlotto-website/internal/models"
|
"synlotto-website/internal/models"
|
||||||
"synlotto-website/internal/platform/config"
|
|
||||||
|
|
||||||
"github.com/gorilla/csrf"
|
"github.com/alexedwards/scs/v2"
|
||||||
|
"github.com/justinas/nosurf"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ToDo should these structs be here?
|
||||||
|
type siteMeta struct {
|
||||||
|
Name string
|
||||||
|
CopyrightYearStart int
|
||||||
|
}
|
||||||
|
|
||||||
|
var meta siteMeta
|
||||||
|
|
||||||
|
func InitSiteMeta(name string, yearStart, yearEnd int) {
|
||||||
|
meta = siteMeta{
|
||||||
|
Name: name,
|
||||||
|
CopyrightYearStart: yearStart,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sm *scs.SessionManager
|
||||||
|
|
||||||
|
func InitSessionManager(manager *scs.SessionManager) {
|
||||||
|
sm = manager
|
||||||
|
}
|
||||||
|
|
||||||
func TemplateContext(w http.ResponseWriter, r *http.Request, data models.TemplateData) map[string]interface{} {
|
func TemplateContext(w http.ResponseWriter, r *http.Request, data models.TemplateData) map[string]interface{} {
|
||||||
cfg := config.Get()
|
|
||||||
if cfg == nil {
|
|
||||||
log.Println("⚠️ Config not initialized!")
|
|
||||||
}
|
|
||||||
session, _ := helpers.GetSession(w, r)
|
|
||||||
|
|
||||||
var flash string
|
|
||||||
if f, ok := session.Values["flash"].(string); ok {
|
|
||||||
flash = f
|
|
||||||
delete(session.Values, "flash")
|
|
||||||
session.Save(r, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"CSRFField": csrf.TemplateField(r),
|
"CSRFToken": nosurf.Token(r),
|
||||||
"Flash": flash,
|
|
||||||
"User": data.User,
|
"User": data.User,
|
||||||
"IsAdmin": data.IsAdmin,
|
"IsAdmin": data.IsAdmin,
|
||||||
"NotificationCount": data.NotificationCount,
|
"NotificationCount": data.NotificationCount,
|
||||||
"Notifications": data.Notifications,
|
"Notifications": data.Notifications,
|
||||||
"MessageCount": data.MessageCount,
|
"MessageCount": data.MessageCount,
|
||||||
"Messages": data.Messages,
|
"Messages": data.Messages,
|
||||||
"SiteName": cfg.Site.SiteName,
|
"SiteName": meta.Name,
|
||||||
"CopyrightYearStart": cfg.Site.CopyrightYearStart,
|
"CopyrightYearStart": meta.CopyrightYearStart,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,9 +62,8 @@ func TemplateFuncs() template.FuncMap {
|
|||||||
"min": func(a, b int) int {
|
"min": func(a, b int) int {
|
||||||
if a < b {
|
if a < b {
|
||||||
return a
|
return a
|
||||||
} else {
|
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
return b
|
||||||
},
|
},
|
||||||
"intVal": func(p *int) int {
|
"intVal": func(p *int) int {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
@@ -97,19 +101,18 @@ func TemplateFuncs() template.FuncMap {
|
|||||||
|
|
||||||
func LoadTemplateFiles(name string, files ...string) *template.Template {
|
func LoadTemplateFiles(name string, files ...string) *template.Template {
|
||||||
shared := []string{
|
shared := []string{
|
||||||
"templates/main/layout.html",
|
"web/templates/main/layout.html",
|
||||||
"templates/main/topbar.html",
|
"web/templates/main/topbar.html",
|
||||||
"templates/main/footer.html",
|
"web/templates/main/footer.html",
|
||||||
}
|
}
|
||||||
all := append(shared, files...)
|
all := append(shared, files...)
|
||||||
|
|
||||||
return template.Must(template.New(name).Funcs(TemplateFuncs()).ParseFiles(all...))
|
return template.Must(template.New(name).Funcs(TemplateFuncs()).ParseFiles(all...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetFlash(w http.ResponseWriter, r *http.Request, message string) {
|
func SetFlash(r *http.Request, message string) {
|
||||||
session, _ := helpers.GetSession(w, r)
|
if sm != nil {
|
||||||
session.Values["flash"] = message
|
sm.Put(r.Context(), "flash", message)
|
||||||
session.Save(r, w)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func InSlice(n int, list []int) bool {
|
func InSlice(n int, list []int) bool {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package helpers
|
package templateHelper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -9,10 +9,12 @@ import (
|
|||||||
"synlotto-website/internal/models"
|
"synlotto-website/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RenderError renders an HTML error page (e.g., 404.html, 500.html).
|
||||||
|
// It uses TemplateContext which reads site meta from InitSiteMeta().
|
||||||
func RenderError(w http.ResponseWriter, r *http.Request, statusCode int) {
|
func RenderError(w http.ResponseWriter, r *http.Request, statusCode int) {
|
||||||
log.Printf("⚙️ RenderError called with status: %d", statusCode)
|
log.Printf("⚙️ RenderError called with status: %d", statusCode)
|
||||||
|
|
||||||
context := TemplateContext(w, r, models.TemplateData{})
|
ctx := TemplateContext(w, r, models.TemplateData{})
|
||||||
|
|
||||||
pagePath := fmt.Sprintf("templates/error/%d.html", statusCode)
|
pagePath := fmt.Sprintf("templates/error/%d.html", statusCode)
|
||||||
log.Printf("📄 Checking for template file: %s", pagePath)
|
log.Printf("📄 Checking for template file: %s", pagePath)
|
||||||
@@ -23,17 +25,14 @@ func RenderError(w http.ResponseWriter, r *http.Request, statusCode int) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("✅ Template file found, loading...")
|
|
||||||
|
|
||||||
tmpl := LoadTemplateFiles(fmt.Sprintf("%d.html", statusCode), pagePath)
|
tmpl := LoadTemplateFiles(fmt.Sprintf("%d.html", statusCode), pagePath)
|
||||||
|
|
||||||
w.WriteHeader(statusCode)
|
w.WriteHeader(statusCode)
|
||||||
err := tmpl.ExecuteTemplate(w, "layout", context)
|
if err := tmpl.ExecuteTemplate(w, "layout", ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Printf("❌ Failed to render error page layout: %v", err)
|
log.Printf("❌ Failed to render error page layout: %v", err)
|
||||||
http.Error(w, http.StatusText(statusCode), statusCode)
|
http.Error(w, http.StatusText(statusCode), statusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("✅ Successfully rendered error page") // ToDo: log these to database
|
log.Println("✅ Successfully rendered error page")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package helpers
|
package templateHelper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ToDo: Sql shouldnt be here.
|
||||||
func GetTotalPages(db *sql.DB, tableName, whereClause string, args []interface{}, pageSize int) (totalPages, totalCount int) {
|
func GetTotalPages(db *sql.DB, tableName, whereClause string, args []interface{}, pageSize int) (totalPages, totalCount int) {
|
||||||
query := "SELECT COUNT(*) FROM " + tableName + " " + whereClause
|
query := "SELECT COUNT(*) FROM " + tableName + " " + whereClause
|
||||||
row := db.QueryRow(query, args...)
|
row := db.QueryRow(query, args...)
|
||||||
|
|||||||
55
internal/http/error/errors.go
Normal file
55
internal/http/error/errors.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
templateHelpers "synlotto-website/internal/helpers/template"
|
||||||
|
"synlotto-website/internal/models"
|
||||||
|
|
||||||
|
"github.com/alexedwards/scs/v2"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RenderStatus renders web/templates/error/<status>.html inside layout.html.
|
||||||
|
func RenderStatus(c *gin.Context, sessions *scs.SessionManager, status int) {
|
||||||
|
// Base context
|
||||||
|
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{})
|
||||||
|
|
||||||
|
// Flash (SCS)
|
||||||
|
if f := sessions.PopString(c.Request.Context(), "flash"); f != "" {
|
||||||
|
ctx["Flash"] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use your finalized paths
|
||||||
|
pagePath := fmt.Sprintf("web/templates/error/%d.html", status)
|
||||||
|
if _, err := os.Stat(pagePath); err != nil {
|
||||||
|
c.String(status, http.StatusText(status))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep your "layout first" load order
|
||||||
|
tmpl := templateHelpers.LoadTemplateFiles(
|
||||||
|
"web/templates/layout.html",
|
||||||
|
pagePath,
|
||||||
|
)
|
||||||
|
|
||||||
|
c.Status(status)
|
||||||
|
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil {
|
||||||
|
c.String(status, http.StatusText(status))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapters so bootstrap can wire these without lambdas everywhere.
|
||||||
|
func NoRoute(sessions *scs.SessionManager) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) { RenderStatus(c, sessions, http.StatusNotFound) }
|
||||||
|
}
|
||||||
|
func NoMethod(sessions *scs.SessionManager) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) { RenderStatus(c, sessions, http.StatusMethodNotAllowed) }
|
||||||
|
}
|
||||||
|
func Recovery(sessions *scs.SessionManager) gin.RecoveryFunc {
|
||||||
|
return func(c *gin.Context, _ interface{}) {
|
||||||
|
RenderStatus(c, sessions, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,48 +2,96 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
httpHelpers "synlotto-website/internal/helpers/http"
|
sessionHelper "synlotto-website/internal/helpers/session"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
"synlotto-website/internal/platform/sessionkeys"
|
||||||
|
|
||||||
"synlotto-website/internal/platform/constants"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Auth(required bool) func(http.HandlerFunc) http.HandlerFunc {
|
// Tracks idle timeout using LastActivity; redirects on timeout.
|
||||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
func AuthMiddleware() gin.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(c *gin.Context) {
|
||||||
session, _ := httpHelpers.GetSession(w, r)
|
app := c.MustGet("app").(*bootstrap.App)
|
||||||
|
sm := app.SessionManager
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
_, ok := session.Values["user_id"].(int)
|
if v := sm.Get(ctx, sessionkeys.LastActivity); v != nil {
|
||||||
|
if last, ok := v.(time.Time); ok && time.Since(last) > sm.Lifetime {
|
||||||
|
_ = sm.RenewToken(ctx)
|
||||||
|
sm.Put(ctx, sessionkeys.Flash, "Your session has timed out.")
|
||||||
|
c.Redirect(http.StatusSeeOther, "/account/login")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if required && !ok {
|
sm.Put(ctx, sessionkeys.LastActivity, time.Now().UTC())
|
||||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional remember-me using selector:verifier token pair.
|
||||||
|
func RememberMiddleware(app *bootstrap.App) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
sm := app.SessionManager
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
// Already logged in? Skip.
|
||||||
|
if sm.Exists(ctx, sessionkeys.UserID) {
|
||||||
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok {
|
cookie, err := c.Request.Cookie(app.Config.Session.RememberCookieName)
|
||||||
last, hasLast := session.Values["last_activity"].(time.Time)
|
if err != nil {
|
||||||
if hasLast && time.Since(last) > constants.SessionDuration {
|
c.Next()
|
||||||
session.Options.MaxAge = -1
|
|
||||||
session.Save(r, w)
|
|
||||||
|
|
||||||
newSession, _ := httpHelpers.GetSession(w, r)
|
|
||||||
newSession.Values["flash"] = "Your session has timed out."
|
|
||||||
newSession.Save(r, w)
|
|
||||||
|
|
||||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
session.Values["last_activity"] = time.Now()
|
parts := strings.SplitN(cookie.Value, ":", 2)
|
||||||
session.Save(r, w)
|
if len(parts) != 2 {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selector, verifier := parts[0], parts[1]
|
||||||
|
|
||||||
|
userID, hash, expires, revokedAt, err := sessionHelper.FindToken(app.DB, selector)
|
||||||
|
if err != nil || revokedAt != nil || time.Now().After(expires) {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
next(w, r)
|
if sessionHelper.HashVerifier(verifier) != hash {
|
||||||
|
// Tampered token – revoke for safety.
|
||||||
|
_ = sessionHelper.RevokeToken(app.DB, selector)
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Success → create fresh SCS session
|
||||||
|
_ = sm.RenewToken(ctx)
|
||||||
|
sm.Put(ctx, sessionkeys.UserID, userID)
|
||||||
|
sm.Put(ctx, sessionkeys.LastActivity, time.Now().UTC())
|
||||||
|
|
||||||
|
c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Protected(h http.HandlerFunc) http.HandlerFunc {
|
// Blocks anonymous users; redirects to login.
|
||||||
return Auth(true)(SessionTimeout(h))
|
func RequireAuth() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
app := c.MustGet("app").(*bootstrap.App)
|
||||||
|
sm := app.SessionManager
|
||||||
|
|
||||||
|
if sm.GetInt(c.Request.Context(), sessionkeys.UserID) == 0 {
|
||||||
|
c.Redirect(http.StatusSeeOther, "/account/login")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
|
// ToDo: make sure im using with gin
|
||||||
import "net/http"
|
import "net/http"
|
||||||
|
|
||||||
func EnforceHTTPS(next http.Handler, enabled bool) http.Handler {
|
func EnforceHTTPS(next http.Handler, enabled bool) http.Handler {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
|
// ToDo: make sure im using with gin
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
|
// ToDo: make sure im using with gin not to be confused with gins recovery but may do the same?
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|||||||
66
internal/http/middleware/remember.go
Normal file
66
internal/http/middleware/remember.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
sessionHelper "synlotto-website/internal/helpers/session"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
"synlotto-website/internal/platform/sessionkeys"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Remember checks if a remember-me cookie exists and restores the session if valid.
|
||||||
|
func Remember(app *bootstrap.App) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
sm := app.SessionManager
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
// Already logged in? skip.
|
||||||
|
if sm.Exists(ctx, sessionkeys.UserID) {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for remember-me cookie
|
||||||
|
cookie, err := c.Request.Cookie(app.Config.Session.RememberCookieName)
|
||||||
|
if err != nil {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(cookie.Value, ":", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selector, verifier := parts[0], parts[1]
|
||||||
|
if selector == "" || verifier == "" {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, hash, expiresAt, revokedAt, err := sessionHelper.FindToken(app.DB, selector)
|
||||||
|
if err != nil || revokedAt != nil || time.Now().After(expiresAt) {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constant-time compare via hashing the verifier
|
||||||
|
if sessionHelper.HashVerifier(verifier) != hash {
|
||||||
|
_ = sessionHelper.RevokeToken(app.DB, selector) // tampered → revoke
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Valid token → create a new session for the user
|
||||||
|
_ = sm.RenewToken(ctx)
|
||||||
|
sm.Put(ctx, sessionkeys.UserID, userID)
|
||||||
|
sm.Put(ctx, sessionkeys.LastActivity, time.Now().UTC())
|
||||||
|
|
||||||
|
// (Optional TODO): rotate token and set a fresh cookie.
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
18
internal/http/middleware/scs.go
Normal file
18
internal/http/middleware/scs.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/alexedwards/scs/v2"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Session(sm *scs.SessionManager) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
handler := sm.LoadAndSave(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
c.Request = r
|
||||||
|
c.Next()
|
||||||
|
}))
|
||||||
|
handler.ServeHTTP(c.Writer, c.Request)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
session "synlotto-website/internal/handlers/session"
|
|
||||||
|
|
||||||
"synlotto-website/internal/platform/constants"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SessionTimeout(next http.HandlerFunc) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
sess, err := session.GetSession(w, r)
|
|
||||||
if err != nil {
|
|
||||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
last, ok := sess.Values["last_activity"].(time.Time)
|
|
||||||
if !ok || time.Since(last) > constants.SessionDuration {
|
|
||||||
sess.Options.MaxAge = -1
|
|
||||||
_ = sess.Save(r, w)
|
|
||||||
|
|
||||||
newSession, _ := session.GetSession(w, r)
|
|
||||||
newSession.Values["flash"] = "Your session has timed out."
|
|
||||||
_ = newSession.Save(r, w)
|
|
||||||
|
|
||||||
log.Printf("Session timeout triggered")
|
|
||||||
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sess.Values["last_activity"] = time.Now().UTC()
|
|
||||||
_ = sess.Save(r, w)
|
|
||||||
|
|
||||||
next(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +1,24 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
accountHandlers "synlotto-website/internal/handlers/account"
|
accountHandlers "synlotto-website/internal/handlers/account"
|
||||||
lotteryDrawHandlers "synlotto-website/internal/handlers/lottery/tickets"
|
|
||||||
|
|
||||||
"synlotto-website/internal/handlers"
|
|
||||||
"synlotto-website/internal/http/middleware"
|
"synlotto-website/internal/http/middleware"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetupAccountRoutes(mux *http.ServeMux, db *sql.DB) {
|
func RegisterAccountRoutes(app *bootstrap.App) {
|
||||||
mux.HandleFunc("/account/login", accountHandlers.Login(db))
|
r := app.Router
|
||||||
mux.HandleFunc("/account/logout", middleware.Protected(accountHandlers.Logout))
|
|
||||||
mux.HandleFunc("/account/signup", accountHandlers.Signup)
|
acc := r.Group("/account")
|
||||||
mux.HandleFunc("/account/tickets/add_ticket", lotteryDrawHandlers.AddTicket(db))
|
acc.GET("/login", accountHandlers.LoginGet)
|
||||||
mux.HandleFunc("/account/tickets/my_tickets", lotteryDrawHandlers.GetMyTickets(db))
|
acc.POST("/login", accountHandlers.LoginPost)
|
||||||
mux.HandleFunc("/account/messages", middleware.Protected(handlers.MessagesInboxHandler(db)))
|
acc.GET("/signup", accountHandlers.SignupGet)
|
||||||
mux.HandleFunc("/account/messages/read", middleware.Protected(handlers.ReadMessageHandler(db)))
|
acc.POST("/signup", accountHandlers.SignupPost)
|
||||||
mux.HandleFunc("/account/messages/archive", middleware.Protected(handlers.ArchiveMessageHandler(db)))
|
|
||||||
mux.HandleFunc("/account/messages/archived", middleware.Protected(handlers.ArchivedMessagesHandler(db)))
|
// Protected logout
|
||||||
mux.HandleFunc("/account/messages/restore", middleware.Protected(handlers.RestoreMessageHandler(db)))
|
accAuth := r.Group("/account")
|
||||||
mux.HandleFunc("/account/messages/send", middleware.Protected(handlers.SendMessageHandler(db)))
|
accAuth.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
||||||
mux.HandleFunc("/account/notifications", middleware.Protected(handlers.NotificationsHandler(db)))
|
accAuth.POST("/logout", accountHandlers.Logout)
|
||||||
mux.HandleFunc("/account/notifications/read", middleware.Protected(handlers.MarkNotificationReadHandler(db)))
|
accAuth.GET("/logout", accountHandlers.Logout) //ToDo: keep if you still support GET?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,38 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
admin "synlotto-website/internal/handlers/admin"
|
admin "synlotto-website/internal/handlers/admin"
|
||||||
|
|
||||||
"synlotto-website/internal/http/middleware"
|
"synlotto-website/internal/http/middleware"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetupAdminRoutes(mux *http.ServeMux, db *sql.DB) {
|
func RegisterAdminRoutes(app *bootstrap.App) {
|
||||||
mux.HandleFunc("/admin/access", middleware.Protected(admin.AdminAccessLogHandler(db)))
|
r := app.Router
|
||||||
mux.HandleFunc("/admin/audit", middleware.Protected(admin.AuditLogHandler(db)))
|
|
||||||
mux.HandleFunc("/admin/dashboard", middleware.Protected(admin.AdminDashboardHandler(db)))
|
adminGroup := r.Group("/admin")
|
||||||
mux.HandleFunc("/admin/triggers", middleware.Protected(admin.AdminTriggersHandler(db)))
|
adminGroup.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
||||||
|
|
||||||
|
// Logs
|
||||||
|
adminGroup.GET("/access", gin.WrapH(admin.AdminAccessLogHandler(app.DB)))
|
||||||
|
adminGroup.GET("/audit", gin.WrapH(admin.AuditLogHandler(app.DB)))
|
||||||
|
|
||||||
|
// Dashboard
|
||||||
|
//adminGroup.GET("/dashboard", gin.WrapH(admin.AdminDashboardHandler(app.DB)))
|
||||||
|
|
||||||
|
// Triggers
|
||||||
|
adminGroup.GET("/triggers", gin.WrapH(admin.AdminTriggersHandler(app.DB)))
|
||||||
|
|
||||||
// Draw management
|
// Draw management
|
||||||
mux.HandleFunc("/admin/draws", middleware.Protected(admin.ListDrawsHandler(db)))
|
adminGroup.GET("/draws", gin.WrapH(admin.ListDrawsHandler(app.DB)))
|
||||||
// mux.HandleFunc("/admin/draws/new", middleware.AdminOnly(db, admin.RenderNewDrawForm(db)))
|
// adminGroup.GET("/draws/new", gin.WrapH(admin.RenderNewDrawForm(app.DB))) // if/when you re-enable AdminOnly
|
||||||
// mux.HandleFunc("/admin/draws/submit", middleware.AdminOnly(db, admin.CreateDrawHandler(db)))
|
// adminGroup.POST("/draws", gin.WrapH(admin.CreateDrawHandler(app.DB))) // example submit route
|
||||||
mux.HandleFunc("/admin/draws/modify", middleware.Protected(admin.ModifyDrawHandler(db)))
|
adminGroup.POST("/draws/modify", gin.WrapH(admin.ModifyDrawHandler(app.DB)))
|
||||||
mux.HandleFunc("/admin/draws/delete", middleware.Protected(admin.DeleteDrawHandler(db)))
|
adminGroup.POST("/draws/delete", gin.WrapH(admin.DeleteDrawHandler(app.DB)))
|
||||||
|
|
||||||
// Prize management
|
// Prize management
|
||||||
mux.HandleFunc("/admin/draws/prizes/add", middleware.Protected(admin.AddPrizesHandler(db)))
|
adminGroup.POST("/draws/prizes/add", gin.WrapH(admin.AddPrizesHandler(app.DB)))
|
||||||
mux.HandleFunc("/admin/draws/prizes/modify", middleware.Protected(admin.ModifyPrizesHandler(db)))
|
adminGroup.POST("/draws/prizes/modify", gin.WrapH(admin.ModifyPrizesHandler(app.DB)))
|
||||||
}
|
}
|
||||||
|
|||||||
10
internal/http/routes/home.go
Normal file
10
internal/http/routes/home.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"synlotto-website/internal/handlers"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterHomeRoutes(app *bootstrap.App) {
|
||||||
|
app.Router.GET("/", handlers.Home(app))
|
||||||
|
}
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
stats "synlotto-website/internal/handlers/statistics"
|
||||||
"net/http"
|
|
||||||
|
|
||||||
handlers "synlotto-website/internal/handlers/statistics"
|
|
||||||
"synlotto-website/internal/http/middleware"
|
"synlotto-website/internal/http/middleware"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetupStatisticsRoutes(mux *http.ServeMux, db *sql.DB) {
|
// RegisterStatisticsRoutes mounts protected statistics endpoints under /statistics.
|
||||||
mux.HandleFunc("/statistics/thunderball", middleware.Auth(true)(handlers.StatisticsThunderball(db)))
|
func RegisterStatisticsRoutes(app *bootstrap.App) {
|
||||||
|
r := app.Router
|
||||||
|
|
||||||
|
group := r.Group("/statistics")
|
||||||
|
group.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
||||||
|
|
||||||
|
group.GET("/thunderball", gin.WrapH(stats.StatisticsThunderball(app)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,33 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
s "synlotto-website/internal/handlers/lottery/syndicate"
|
||||||
"net/http"
|
|
||||||
|
|
||||||
lotterySyndicateHandlers "synlotto-website/internal/handlers/lottery/syndicate"
|
|
||||||
|
|
||||||
"synlotto-website/internal/http/middleware"
|
"synlotto-website/internal/http/middleware"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetupSyndicateRoutes(mux *http.ServeMux, db *sql.DB) {
|
// RegisterSyndicateRoutes mounts all /syndicate routes.
|
||||||
mux.HandleFunc("/syndicate", middleware.Auth(true)(lotterySyndicateHandlers.ListSyndicatesHandler(db)))
|
// Protection is enforced at the group level via AuthMiddleware + RequireAuth.
|
||||||
mux.HandleFunc("/syndicate/create", middleware.Auth(true)(lotterySyndicateHandlers.CreateSyndicateHandler(db)))
|
func RegisterSyndicateRoutes(app *bootstrap.App) {
|
||||||
mux.HandleFunc("/syndicate/view", middleware.Auth(true)(lotterySyndicateHandlers.ViewSyndicateHandler(db)))
|
r := app.Router
|
||||||
mux.HandleFunc("/syndicate/tickets", middleware.Auth(true)(lotterySyndicateHandlers.SyndicateTicketsHandler(db)))
|
|
||||||
mux.HandleFunc("/syndicate/tickets/new", middleware.Auth(true)(lotterySyndicateHandlers.SyndicateLogTicketHandler(db)))
|
|
||||||
mux.HandleFunc("/syndicate/invites", middleware.Auth(true)(lotterySyndicateHandlers.ViewInvitesHandler(db)))
|
|
||||||
mux.HandleFunc("/syndicate/invites/accept", middleware.Auth(true)(lotterySyndicateHandlers.AcceptInviteHandler(db)))
|
|
||||||
mux.HandleFunc("/syndicate/invites/decline", middleware.Auth(true)(lotterySyndicateHandlers.DeclineInviteHandler(db)))
|
|
||||||
mux.HandleFunc("/syndicate/invite/token", middleware.Auth(true)(lotterySyndicateHandlers.GenerateInviteLinkHandler(db)))
|
|
||||||
mux.HandleFunc("/syndicate/invite/tokens", middleware.Auth(true)(lotterySyndicateHandlers.ManageInviteTokensHandler(db)))
|
|
||||||
mux.HandleFunc("/syndicate/join", middleware.Auth(true)(lotterySyndicateHandlers.JoinSyndicateWithTokenHandler(db)))
|
|
||||||
|
|
||||||
|
syn := r.Group("/syndicate")
|
||||||
|
syn.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
||||||
|
|
||||||
|
// Use Any to preserve old ServeMux behavior (accepts both GET/POST where applicable).
|
||||||
|
// You can refine methods later (e.g., GET for views, POST for mutate actions).
|
||||||
|
syn.Any("", gin.WrapH(s.ListSyndicatesHandler(app)))
|
||||||
|
syn.Any("/create", gin.WrapH(s.CreateSyndicateHandler(app)))
|
||||||
|
syn.Any("/view", gin.WrapH(s.ViewSyndicateHandler(app)))
|
||||||
|
syn.Any("/tickets", gin.WrapH(s.SyndicateTicketsHandler(app)))
|
||||||
|
syn.Any("/tickets/new", gin.WrapH(s.SyndicateLogTicketHandler(app)))
|
||||||
|
syn.Any("/invites", gin.WrapH(s.ViewInvitesHandler(app)))
|
||||||
|
syn.Any("/invites/accept", gin.WrapH(s.AcceptInviteHandler(app)))
|
||||||
|
syn.Any("/invites/decline", gin.WrapH(s.DeclineInviteHandler(app)))
|
||||||
|
syn.Any("/invite/token", gin.WrapH(s.GenerateInviteLinkHandler(app)))
|
||||||
|
syn.Any("/invite/tokens", gin.WrapH(s.ManageInviteTokensHandler(app)))
|
||||||
|
syn.Any("/join", gin.WrapH(s.JoinSyndicateWithTokenHandler(app)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,11 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"synlotto-website/internal/models"
|
"synlotto-website/internal/platform/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LogConfig(config *models.Config) {
|
func LogConfig(config *config.Config) {
|
||||||
safeConfig := *config
|
safeConfig := *config
|
||||||
safeConfig.CSRF.CSRFKey = "[REDACTED]"
|
|
||||||
safeConfig.Session.AuthKeyPath = "[REDACTED]"
|
|
||||||
safeConfig.Session.EncryptionKeyPath = "[REDACTED]"
|
|
||||||
|
|
||||||
cfg, err := json.MarshalIndent(safeConfig, "", " ")
|
cfg, err := json.MarshalIndent(safeConfig, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Id int
|
Id int64
|
||||||
Username string
|
Username string
|
||||||
|
Email string
|
||||||
PasswordHash string
|
PasswordHash string
|
||||||
IsAdmin bool
|
IsAdmin bool
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToDo: should be in a notification model?
|
// ToDo: should be in a notification model?
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
package bootstrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/csrf"
|
|
||||||
)
|
|
||||||
|
|
||||||
var CSRFMiddleware func(http.Handler) http.Handler
|
|
||||||
|
|
||||||
func InitCSRFProtection(csrfKey []byte, isProduction bool) error {
|
|
||||||
if len(csrfKey) != 32 {
|
|
||||||
return fmt.Errorf("csrf key must be 32 bytes, got %d", len(csrfKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
CSRFMiddleware = csrf.Protect(
|
|
||||||
csrfKey,
|
|
||||||
csrf.Secure(isProduction),
|
|
||||||
csrf.SameSite(csrf.SameSiteStrictMode),
|
|
||||||
csrf.Path("/"),
|
|
||||||
csrf.HttpOnly(true),
|
|
||||||
)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -5,12 +5,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
internal "synlotto-website/internal/licensecheck"
|
internal "synlotto-website/internal/licensecheck"
|
||||||
"synlotto-website/internal/models"
|
"synlotto-website/internal/platform/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var globalChecker *internal.LicenseChecker
|
var globalChecker *internal.LicenseChecker
|
||||||
|
|
||||||
func InitLicenseChecker(config *models.Config) error {
|
func InitLicenseChecker(config *config.Config) error {
|
||||||
checker := &internal.LicenseChecker{
|
checker := &internal.LicenseChecker{
|
||||||
LicenseAPIURL: config.License.APIURL,
|
LicenseAPIURL: config.License.APIURL,
|
||||||
APIKey: config.License.APIKey,
|
APIKey: config.License.APIKey,
|
||||||
|
|||||||
@@ -1,30 +1,160 @@
|
|||||||
|
// Package bootstrap
|
||||||
|
// Path /internal/platform/bootstrap
|
||||||
|
// File: loader.go
|
||||||
|
//
|
||||||
|
// Purpose:
|
||||||
|
// Centralized application initializer (the “application kernel”).
|
||||||
|
// This constructs and wires together the core runtime graph used by the
|
||||||
|
// entire system: configuration, database, session manager (SCS), router (Gin),
|
||||||
|
// CSRF wrapper (nosurf), and the HTTP server.
|
||||||
|
//
|
||||||
|
// Responsibilities:
|
||||||
|
// 1) Load strongly-typed configuration.
|
||||||
|
// 2) Initialize long-lived infrastructure (DB, sessions).
|
||||||
|
// 3) Build the Gin router and mount global middleware and routes.
|
||||||
|
// 4) Wrap the router with SCS (LoadAndSave) and CSRF in the correct order.
|
||||||
|
// 5) Construct the http.Server and expose the assembled components via App.
|
||||||
|
//
|
||||||
|
// HTTP stack order (important):
|
||||||
|
// Gin Router → SCS LoadAndSave → CSRF Wrapper → http.Server
|
||||||
|
//
|
||||||
|
// Design guarantees:
|
||||||
|
// - Single source of truth via the App struct.
|
||||||
|
// - Stable middleware order (SCS must wrap Gin before CSRF).
|
||||||
|
// - Gin handlers can access *App via c.MustGet("app").
|
||||||
|
// - Extensible: add infra (cache/mailer/metrics) here.
|
||||||
|
//
|
||||||
|
// Change log:
|
||||||
|
// [2025-10-24] Migrated to SCS-first wrapping and explicit App wiring.
|
||||||
|
|
||||||
package bootstrap
|
package bootstrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"synlotto-website/internal/models"
|
weberr "synlotto-website/internal/http/error"
|
||||||
|
databasePlatform "synlotto-website/internal/platform/database"
|
||||||
|
|
||||||
|
"synlotto-website/internal/platform/config"
|
||||||
|
"synlotto-website/internal/platform/csrf"
|
||||||
|
"synlotto-website/internal/platform/session"
|
||||||
|
|
||||||
|
"github.com/alexedwards/scs/v2"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppState struct {
|
type App struct {
|
||||||
Config *models.Config
|
Config config.Config
|
||||||
|
DB *sql.DB
|
||||||
|
SessionManager *scs.SessionManager
|
||||||
|
Router *gin.Engine
|
||||||
|
Handler http.Handler
|
||||||
|
Server *http.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadAppState(configPath string) (*AppState, error) {
|
func Load(configPath string) (*App, error) {
|
||||||
file, err := os.Open(configPath)
|
cfg, err := config.Load(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("open config: %w", err)
|
return nil, fmt.Errorf("load config: %w", err)
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
var config models.Config
|
|
||||||
if err := json.NewDecoder(file).Decode(&config); err != nil {
|
|
||||||
return nil, fmt.Errorf("decode config: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &AppState{
|
db, err := openMySQL(cfg)
|
||||||
Config: &config,
|
if err != nil {
|
||||||
}, nil
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := db.PingContext(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("mysql ping: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := databasePlatform.EnsureInitialSchema(db); err != nil {
|
||||||
|
return nil, fmt.Errorf("ensure schema: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions := session.New(cfg)
|
||||||
|
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(gin.Logger(), gin.Recovery())
|
||||||
|
router.Static("/static", "./web/static")
|
||||||
|
router.StaticFile("/favicon.ico", "./web/static/favicon.ico")
|
||||||
|
|
||||||
|
app := &App{
|
||||||
|
Config: cfg,
|
||||||
|
DB: db,
|
||||||
|
SessionManager: sessions,
|
||||||
|
Router: router,
|
||||||
|
}
|
||||||
|
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("app", app)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
router.NoRoute(weberr.NoRoute(app.SessionManager))
|
||||||
|
router.NoMethod(weberr.NoMethod(app.SessionManager))
|
||||||
|
router.Use(gin.CustomRecovery(weberr.Recovery(app.SessionManager)))
|
||||||
|
|
||||||
|
handler := sessions.LoadAndSave(router)
|
||||||
|
handler = csrf.Wrap(handler, cfg)
|
||||||
|
|
||||||
|
addr := fmt.Sprintf("%s:%d", cfg.HttpServer.Address, cfg.HttpServer.Port)
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: handler,
|
||||||
|
ReadHeaderTimeout: 10 * time.Second, // ToDo: consider moving to config
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Handler = handler
|
||||||
|
app.Server = srv
|
||||||
|
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openMySQL(cfg config.Config) (*sql.DB, error) {
|
||||||
|
dbCfg := cfg.Database
|
||||||
|
|
||||||
|
escapedUser := dbCfg.Username
|
||||||
|
escapedPass := dbCfg.Password
|
||||||
|
|
||||||
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&charset=utf8mb4,utf8&loc=UTC",
|
||||||
|
escapedUser,
|
||||||
|
escapedPass,
|
||||||
|
dbCfg.Server,
|
||||||
|
dbCfg.Port,
|
||||||
|
dbCfg.DatabaseNamed,
|
||||||
|
)
|
||||||
|
|
||||||
|
db, err := sql.Open("mysql", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("mysql open: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dbCfg.MaxOpenConnections > 0 {
|
||||||
|
db.SetMaxOpenConns(dbCfg.MaxOpenConnections)
|
||||||
|
}
|
||||||
|
if dbCfg.MaxIdleConnections > 0 {
|
||||||
|
db.SetMaxIdleConns(dbCfg.MaxIdleConnections)
|
||||||
|
}
|
||||||
|
if dbCfg.ConnectionMaxLifetime != "" {
|
||||||
|
if d, err := time.ParseDuration(dbCfg.ConnectionMaxLifetime); err == nil {
|
||||||
|
db.SetConnMaxLifetime(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := db.PingContext(ctx); err != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
return nil, fmt.Errorf("mysql ping: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
package bootstrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/gob"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
sessionHandlers "synlotto-website/internal/handlers/session"
|
|
||||||
sessionHelpers "synlotto-website/internal/helpers/session"
|
|
||||||
|
|
||||||
"synlotto-website/internal/logging"
|
|
||||||
"synlotto-website/internal/models"
|
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
sessionStore *sessions.CookieStore
|
|
||||||
Name string
|
|
||||||
authKey []byte
|
|
||||||
encryptKey []byte
|
|
||||||
)
|
|
||||||
|
|
||||||
func InitSession(cfg *models.Config) error {
|
|
||||||
gob.Register(time.Time{})
|
|
||||||
authPath := cfg.Session.AuthKeyPath
|
|
||||||
encPath := cfg.Session.EncryptionKeyPath
|
|
||||||
|
|
||||||
if _, err := os.Stat(authPath); os.IsNotExist(err) {
|
|
||||||
logging.Info("⚠️ Auth key not found, creating: %s", authPath)
|
|
||||||
key, err := generateRandomBytes(32)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
encoded := sessionHelpers.EncodeKey(key)
|
|
||||||
err = os.WriteFile(authPath, []byte(encoded), 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stat(encPath); os.IsNotExist(err) {
|
|
||||||
logging.Info("⚠️ Encryption key not found, creating: %s", encPath)
|
|
||||||
key, err := generateRandomBytes(32)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
encoded := sessionHelpers.EncodeKey(key)
|
|
||||||
err = os.WriteFile(encPath, []byte(encoded), 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return loadSessionKeys(
|
|
||||||
authPath,
|
|
||||||
encPath,
|
|
||||||
cfg.Session.Name,
|
|
||||||
cfg.HttpServer.ProductionMode,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateRandomBytes(length int) ([]byte, error) {
|
|
||||||
b := make([]byte, length)
|
|
||||||
_, err := rand.Read(b)
|
|
||||||
if err != nil {
|
|
||||||
logging.Error("failed to generate random bytes: %w", err)
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadSessionKeys(authPath, encryptionPath, name string, isProduction bool) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
rawAuth, err := os.ReadFile(authPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error reading auth key: %w", err)
|
|
||||||
}
|
|
||||||
authKey, err = base64.StdEncoding.DecodeString(string(bytes.TrimSpace(rawAuth)))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error decoding auth key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rawEnc, err := os.ReadFile(encryptionPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error reading encryption key: %w", err)
|
|
||||||
}
|
|
||||||
encryptKey, err = base64.StdEncoding.DecodeString(string(bytes.TrimSpace(rawEnc)))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error decoding encryption key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(authKey) != 32 || len(encryptKey) != 32 {
|
|
||||||
return fmt.Errorf("auth and encryption keys must be 32 bytes each (got auth=%d, enc=%d)", len(authKey), len(encryptKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionHandlers.SessionStore = sessions.NewCookieStore(authKey, encryptKey)
|
|
||||||
sessionHandlers.SessionStore.Options = &sessions.Options{
|
|
||||||
Path: "/",
|
|
||||||
MaxAge: 86400,
|
|
||||||
HttpOnly: true,
|
|
||||||
Secure: isProduction,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionHandlers.Name = name
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -2,21 +2,19 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"synlotto-website/internal/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
appConfig *models.Config
|
appConfig *Config
|
||||||
once sync.Once
|
once sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init(config *models.Config) {
|
func Init(config *Config) {
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
appConfig = config
|
appConfig = config
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Get() *models.Config {
|
func Get() *Config {
|
||||||
return appConfig
|
return appConfig
|
||||||
}
|
}
|
||||||
|
|||||||
35
internal/platform/config/config.json
Normal file
35
internal/platform/config/config.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"csrf": {
|
||||||
|
"cookieName": ""
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"server": "",
|
||||||
|
"port": 3306,
|
||||||
|
"databaseName": "",
|
||||||
|
"maxOpenConnections": 10,
|
||||||
|
"maxIdleConnections": 5,
|
||||||
|
"connectionMaxLifetime": "",
|
||||||
|
"username": "",
|
||||||
|
"password":""
|
||||||
|
},
|
||||||
|
"httpServer": {
|
||||||
|
"port": 8082,
|
||||||
|
"address": "",
|
||||||
|
"productionMode": false
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"apiUrl": "",
|
||||||
|
"apiKey": ""
|
||||||
|
},
|
||||||
|
"session": {
|
||||||
|
"cookieName": "",
|
||||||
|
"lifetime": "",
|
||||||
|
"idleTimeout": "",
|
||||||
|
"rememberCookieName": "",
|
||||||
|
"rememberDuration": ""
|
||||||
|
},
|
||||||
|
"site": {
|
||||||
|
"siteName": "",
|
||||||
|
"copyrightYearStart": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
21
internal/platform/config/load.go
Normal file
21
internal/platform/config/load.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Load(path string) (Config, error) {
|
||||||
|
var cfg Config
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package models
|
package config
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
CSRF struct {
|
CSRF struct {
|
||||||
CSRFKey string `json:"csrfKey"`
|
CookieName string `json:"cookieName"`
|
||||||
} `json:"csrf"`
|
} `json:"csrf"`
|
||||||
|
|
||||||
Database struct {
|
Database struct {
|
||||||
@@ -28,9 +28,11 @@ type Config struct {
|
|||||||
} `json:"license"`
|
} `json:"license"`
|
||||||
|
|
||||||
Session struct {
|
Session struct {
|
||||||
AuthKeyPath string `json:"authKeyPath"`
|
CookieName string `json:"cookieName"`
|
||||||
EncryptionKeyPath string `json:"encryptionKeyPath"`
|
Lifetime string `json:"lifetime"`
|
||||||
Name string `json:"name"`
|
IdleTimeout string `json:"idleTimeout"`
|
||||||
|
RememberCookieName string `json:"rememberCookieName"`
|
||||||
|
RememberDuration string `json:"rememberDuration"`
|
||||||
} `json:"session"`
|
} `json:"session"`
|
||||||
|
|
||||||
Site struct {
|
Site struct {
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package constants
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
const SessionDuration = 30 * time.Minute
|
|
||||||
21
internal/platform/csrf/csrf.go
Normal file
21
internal/platform/csrf/csrf.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package csrf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"synlotto-website/internal/platform/config"
|
||||||
|
|
||||||
|
"github.com/justinas/nosurf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Wrap(h http.Handler, cfg config.Config) http.Handler {
|
||||||
|
cs := nosurf.New(h)
|
||||||
|
cs.SetBaseCookie(http.Cookie{
|
||||||
|
Name: cfg.CSRF.CookieName,
|
||||||
|
Path: "/",
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: cfg.HttpServer.ProductionMode,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
})
|
||||||
|
return cs
|
||||||
|
}
|
||||||
35
internal/platform/database/schema.go
Normal file
35
internal/platform/database/schema.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package databasePlatform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
databaseHelpers "synlotto-website/internal/helpers/database"
|
||||||
|
migrationSQL "synlotto-website/internal/storage/migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EnsureInitialSchema(db *sql.DB) error {
|
||||||
|
fmt.Println("✅ EnsureInitialSchema called") // temp debug
|
||||||
|
|
||||||
|
var cnt int
|
||||||
|
if err := db.QueryRow(migrationSQL.ProbeUsersTable).Scan(&cnt); err != nil {
|
||||||
|
return fmt.Errorf("probe users table failed: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("👀 Probe users count = %d\n", cnt) // temp debug
|
||||||
|
if cnt > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanity: show embedded SQL length so we know it actually embedded
|
||||||
|
fmt.Printf("📦 Initial SQL bytes: %d\n", len(migrationSQL.InitialSchema)) // temp debug
|
||||||
|
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := databaseHelpers.ExecScript(tx, migrationSQL.InitialSchema); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return fmt.Errorf("apply schema: %w", err)
|
||||||
|
}
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
35
internal/platform/session/session.go
Normal file
35
internal/platform/session/session.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"synlotto-website/internal/platform/config"
|
||||||
|
|
||||||
|
"github.com/alexedwards/scs/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(cfg config.Config) *scs.SessionManager {
|
||||||
|
gob.Register(time.Time{})
|
||||||
|
s := scs.New()
|
||||||
|
|
||||||
|
// Lifetime (absolute max age)
|
||||||
|
if d, err := time.ParseDuration(cfg.Session.Lifetime); err == nil && d > 0 {
|
||||||
|
s.Lifetime = d
|
||||||
|
} else {
|
||||||
|
s.Lifetime = 12 * time.Hour
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idle timeout (expire after inactivity)
|
||||||
|
if d, err := time.ParseDuration(cfg.Session.IdleTimeout); err == nil && d > 0 {
|
||||||
|
s.IdleTimeout = d
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Cookie.Name = cfg.Session.CookieName
|
||||||
|
s.Cookie.HttpOnly = true
|
||||||
|
s.Cookie.SameSite = http.SameSiteLaxMode
|
||||||
|
s.Cookie.Secure = cfg.HttpServer.ProductionMode
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
8
internal/platform/sessionkeys/keys.go
Normal file
8
internal/platform/sessionkeys/keys.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package sessionkeys
|
||||||
|
|
||||||
|
//ToDo: Is this just putting in "user_id" rather than the users ID?
|
||||||
|
const (
|
||||||
|
UserID = "user_id"
|
||||||
|
LastActivity = "last_activity"
|
||||||
|
Flash = "flash"
|
||||||
|
)
|
||||||
@@ -5,14 +5,15 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
lotteryTicketHandlers "synlotto-website/internal/handlers/lottery/tickets"
|
|
||||||
thunderballrules "synlotto-website/internal/rules/thunderball"
|
thunderballrules "synlotto-website/internal/rules/thunderball"
|
||||||
services "synlotto-website/internal/services/draws"
|
drawsSvc "synlotto-website/internal/services/draws"
|
||||||
|
|
||||||
"synlotto-website/internal/helpers"
|
"synlotto-website/internal/helpers"
|
||||||
"synlotto-website/internal/models"
|
"synlotto-website/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RunTicketMatching finds unmatched tickets, matches them to draw results,
|
||||||
|
// updates match/prize fields, and writes a summary log entry.
|
||||||
func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, error) {
|
func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, error) {
|
||||||
stats := models.MatchRunStats{}
|
stats := models.MatchRunStats{}
|
||||||
|
|
||||||
@@ -29,11 +30,9 @@ func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, er
|
|||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var pending []models.Ticket
|
var pending []models.Ticket
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var t models.Ticket
|
var t models.Ticket
|
||||||
var b1, b2, b3, b4, b5, b6, bo1, bo2 sql.NullInt64
|
var b1, b2, b3, b4, b5, b6, bo1, bo2 sql.NullInt64
|
||||||
|
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&t.Id, &t.GameType, &t.DrawDate,
|
&t.Id, &t.GameType, &t.DrawDate,
|
||||||
&b1, &b2, &b3, &b4, &b5, &b6,
|
&b1, &b2, &b3, &b4, &b5, &b6,
|
||||||
@@ -41,7 +40,6 @@ func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, er
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Ball1 = int(b1.Int64)
|
t.Ball1 = int(b1.Int64)
|
||||||
t.Ball2 = int(b2.Int64)
|
t.Ball2 = int(b2.Int64)
|
||||||
t.Ball3 = int(b3.Int64)
|
t.Ball3 = int(b3.Int64)
|
||||||
@@ -56,32 +54,32 @@ func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, er
|
|||||||
|
|
||||||
for _, t := range pending {
|
for _, t := range pending {
|
||||||
matchTicket := models.MatchTicket{
|
matchTicket := models.MatchTicket{
|
||||||
ID: t.Id,
|
|
||||||
GameType: t.GameType,
|
|
||||||
DrawDate: t.DrawDate,
|
|
||||||
Balls: helpers.BuildBallsSlice(t),
|
Balls: helpers.BuildBallsSlice(t),
|
||||||
BonusBalls: helpers.BuildBonusSlice(t),
|
BonusBalls: helpers.BuildBonusSlice(t),
|
||||||
}
|
}
|
||||||
|
|
||||||
draw := services.GetDrawResultForTicket(db, t.GameType, t.DrawDate)
|
draw := drawsSvc.GetDrawResultForTicket(db, t.GameType, t.DrawDate)
|
||||||
result := lotteryTicketHandlers.MatchTicketToDraw(matchTicket, draw, thunderballrules.ThunderballPrizeRules)
|
if draw.DrawID == 0 {
|
||||||
|
// No draw yet → skip
|
||||||
if result.MatchedDrawID == 0 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := db.Exec(`
|
mainMatches := helpers.CountMatches(matchTicket.Balls, draw.Balls)
|
||||||
|
bonusMatches := helpers.CountMatches(matchTicket.BonusBalls, draw.BonusBalls)
|
||||||
|
prizeTier := GetPrizeTier(matchTicket.GameType, mainMatches, bonusMatches, thunderballrules.ThunderballPrizeRules)
|
||||||
|
isWinner := prizeTier != ""
|
||||||
|
|
||||||
|
if _, err := db.Exec(`
|
||||||
UPDATE my_tickets
|
UPDATE my_tickets
|
||||||
SET matched_main = ?, matched_bonus = ?, prize_tier = ?, is_winner = ?
|
SET matched_main = ?, matched_bonus = ?, prize_tier = ?, is_winner = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`, result.MatchedMain, result.MatchedBonus, result.PrizeTier, result.IsWinner, t.Id)
|
`, mainMatches, bonusMatches, prizeTier, isWinner, t.Id); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Println("⚠️ Failed to update ticket match:", err)
|
log.Println("⚠️ Failed to update ticket match:", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.TicketsMatched++
|
stats.TicketsMatched++
|
||||||
if result.IsWinner {
|
if isWinner {
|
||||||
stats.WinnersFound++
|
stats.WinnersFound++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,6 +92,7 @@ func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, er
|
|||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateMissingPrizes fills in prize labels/amounts for already-matched winners that lack labels.
|
||||||
func UpdateMissingPrizes(db *sql.DB) error {
|
func UpdateMissingPrizes(db *sql.DB) error {
|
||||||
type TicketInfo struct {
|
type TicketInfo struct {
|
||||||
ID int
|
ID int
|
||||||
@@ -138,8 +137,7 @@ func UpdateMissingPrizes(db *sql.DB) error {
|
|||||||
query := fmt.Sprintf(`SELECT prize%d_per_winner FROM prizes_thunderball WHERE draw_date = ?`, idx)
|
query := fmt.Sprintf(`SELECT prize%d_per_winner FROM prizes_thunderball WHERE draw_date = ?`, idx)
|
||||||
|
|
||||||
var amount int
|
var amount int
|
||||||
err := db.QueryRow(query, t.DrawDate).Scan(&amount)
|
if err := db.QueryRow(query, t.DrawDate).Scan(&amount); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Printf("❌ Prize lookup failed for ticket %d: %v", t.ID, err)
|
log.Printf("❌ Prize lookup failed for ticket %d: %v", t.ID, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -149,11 +147,9 @@ func UpdateMissingPrizes(db *sql.DB) error {
|
|||||||
label = fmt.Sprintf("£%.2f", float64(amount))
|
label = fmt.Sprintf("£%.2f", float64(amount))
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.Exec(`
|
if _, err := db.Exec(`
|
||||||
UPDATE my_tickets SET prize_amount = ?, prize_label = ? WHERE id = ?
|
UPDATE my_tickets SET prize_amount = ?, prize_label = ? WHERE id = ?
|
||||||
`, float64(amount), label, t.ID)
|
`, float64(amount), label, t.ID); err != nil {
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("❌ Failed to update ticket %d: %v", t.ID, err)
|
log.Printf("❌ Failed to update ticket %d: %v", t.ID, err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("✅ Updated ticket %d → %s", t.ID, label)
|
log.Printf("✅ Updated ticket %d → %s", t.ID, label)
|
||||||
@@ -163,6 +159,7 @@ func UpdateMissingPrizes(db *sql.DB) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RefreshTicketPrizes recomputes and writes prize info for all tickets.
|
||||||
func RefreshTicketPrizes(db *sql.DB) error {
|
func RefreshTicketPrizes(db *sql.DB) error {
|
||||||
type TicketRow struct {
|
type TicketRow struct {
|
||||||
ID int
|
ID int
|
||||||
@@ -198,13 +195,11 @@ func RefreshTicketPrizes(db *sql.DB) error {
|
|||||||
|
|
||||||
for _, row := range tickets {
|
for _, row := range tickets {
|
||||||
matchTicket := models.MatchTicket{
|
matchTicket := models.MatchTicket{
|
||||||
GameType: row.GameType,
|
|
||||||
DrawDate: row.DrawDate,
|
|
||||||
Balls: helpers.BuildBallsFromNulls(row.B1, row.B2, row.B3, row.B4, row.B5, row.B6),
|
Balls: helpers.BuildBallsFromNulls(row.B1, row.B2, row.B3, row.B4, row.B5, row.B6),
|
||||||
BonusBalls: helpers.BuildBonusFromNulls(row.Bonus1, row.Bonus2),
|
BonusBalls: helpers.BuildBonusFromNulls(row.Bonus1, row.Bonus2),
|
||||||
}
|
}
|
||||||
|
|
||||||
draw := services.GetDrawResultForTicket(db, row.GameType, row.DrawDate)
|
draw := drawsSvc.GetDrawResultForTicket(db, row.GameType, row.DrawDate)
|
||||||
if draw.DrawID == 0 {
|
if draw.DrawID == 0 {
|
||||||
log.Printf("❌ No draw result for %s (%s)", row.DrawDate, row.GameType)
|
log.Printf("❌ No draw result for %s (%s)", row.DrawDate, row.GameType)
|
||||||
continue
|
continue
|
||||||
@@ -212,18 +207,16 @@ func RefreshTicketPrizes(db *sql.DB) error {
|
|||||||
|
|
||||||
mainMatches := helpers.CountMatches(matchTicket.Balls, draw.Balls)
|
mainMatches := helpers.CountMatches(matchTicket.Balls, draw.Balls)
|
||||||
bonusMatches := helpers.CountMatches(matchTicket.BonusBalls, draw.BonusBalls)
|
bonusMatches := helpers.CountMatches(matchTicket.BonusBalls, draw.BonusBalls)
|
||||||
prizeTier := matcher.GetPrizeTier(row.GameType, mainMatches, bonusMatches, thunderballrules.ThunderballPrizeRules)
|
prizeTier := GetPrizeTier(row.GameType, mainMatches, bonusMatches, thunderballrules.ThunderballPrizeRules)
|
||||||
isWinner := prizeTier != ""
|
isWinner := prizeTier != ""
|
||||||
|
|
||||||
var label string
|
var label string
|
||||||
var amount float64
|
var amount float64
|
||||||
if row.GameType == "Thunderball" {
|
if row.GameType == "Thunderball" {
|
||||||
idx, ok := thunderballrules.GetThunderballPrizeIndex(mainMatches, bonusMatches)
|
if idx, ok := thunderballrules.GetThunderballPrizeIndex(mainMatches, bonusMatches); ok {
|
||||||
if ok {
|
|
||||||
query := fmt.Sprintf(`SELECT prize%d_per_winner FROM prizes_thunderball WHERE draw_date = ?`, idx)
|
query := fmt.Sprintf(`SELECT prize%d_per_winner FROM prizes_thunderball WHERE draw_date = ?`, idx)
|
||||||
var val int
|
var val int
|
||||||
err := db.QueryRow(query, row.DrawDate).Scan(&val)
|
if err := db.QueryRow(query, row.DrawDate).Scan(&val); err == nil {
|
||||||
if err == nil {
|
|
||||||
amount = float64(val)
|
amount = float64(val)
|
||||||
if val > 0 {
|
if val > 0 {
|
||||||
label = fmt.Sprintf("£%.2f", amount)
|
label = fmt.Sprintf("£%.2f", amount)
|
||||||
@@ -242,16 +235,16 @@ func RefreshTicketPrizes(db *sql.DB) error {
|
|||||||
SET matched_main = ?, matched_bonus = ?, prize_tier = ?, is_winner = ?, prize_amount = ?, prize_label = ?
|
SET matched_main = ?, matched_bonus = ?, prize_tier = ?, is_winner = ?, prize_amount = ?, prize_label = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`, mainMatches, bonusMatches, prizeTier, isWinner, amount, label, row.ID)
|
`, mainMatches, bonusMatches, prizeTier, isWinner, amount, label, row.ID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("❌ Failed to update ticket %d: %v", row.ID, err)
|
log.Printf("❌ Failed to update ticket %d: %v", row.ID, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
rowsAffected, _ := res.RowsAffected()
|
if rowsAffected, _ := res.RowsAffected(); rowsAffected > 0 {
|
||||||
log.Printf("✅ Ticket %d updated — rows affected: %d | Tier: %s | Label: %s | Matches: %d+%d",
|
log.Printf("✅ Ticket %d updated — rows affected: %d | Tier: %s | Label: %s | Matches: %d+%d",
|
||||||
row.ID, rowsAffected, prizeTier, label, mainMatches, bonusMatches)
|
row.ID, rowsAffected, prizeTier, label, mainMatches, bonusMatches)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
84
internal/storage/auditlog/create.go
Normal file
84
internal/storage/auditlog/create.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
securityHelpers "synlotto-website/internal/helpers/security"
|
||||||
|
|
||||||
|
"synlotto-website/internal/logging"
|
||||||
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const insertRegistrationSQL = `
|
||||||
|
INSERT INTO audit_registration
|
||||||
|
(user_id, username, email, ip, user_agent, timestamp)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`
|
||||||
|
|
||||||
|
func AdminOnly() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
app := c.MustGet("app").(*bootstrap.App)
|
||||||
|
sm := app.SessionManager
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
// Require logged in (assumes RequireAuth already ran; this is a safety net)
|
||||||
|
v := sm.Get(ctx, "user_id")
|
||||||
|
var uid int64
|
||||||
|
switch t := v.(type) {
|
||||||
|
case int64:
|
||||||
|
uid = t
|
||||||
|
case int:
|
||||||
|
uid = int64(t)
|
||||||
|
default:
|
||||||
|
c.Redirect(http.StatusSeeOther, "/account/login")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check admin
|
||||||
|
if !securityHelpers.IsAdmin(app.DB, int(uid)) {
|
||||||
|
// Optional: log access attempt here or in a helper
|
||||||
|
c.String(http.StatusForbidden, "Forbidden")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally record access (moved here from storage)
|
||||||
|
_, _ = app.DB.Exec(`
|
||||||
|
INSERT INTO admin_access_log (user_id, path, ip, user_agent, accessed_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
`, uid, c.Request.URL.Path, c.ClientIP(), c.Request.UserAgent(), time.Now().UTC())
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Todo has to add in - db *sql.DB to make this work should this not be an import as all functions use it, more importantly no functions in storage just sql?
|
||||||
|
// Handler Call - auditlogStorage.LogLoginAttempt(db, r.RemoteAddr, r.UserAgent(), username, ok)
|
||||||
|
func LogLoginAttempt(db *sql.DB, rIP, rUA, username string, success bool) {
|
||||||
|
_, err := db.Exec(
|
||||||
|
`INSERT INTO audit_login (username, success, ip, user_agent, timestamp)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)`,
|
||||||
|
username, success, rIP, rUA, time.Now().UTC(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
logging.Info("❌ Failed to log login:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LogSignup(db *sql.DB, userID int64, username, email, ip, userAgent string) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := db.ExecContext(ctx, insertRegistrationSQL,
|
||||||
|
userID, username, email, ip, userAgent, time.Now().UTC(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
logging.Info("❌ Failed to log registration: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
3
internal/storage/messages/delete.go
Normal file
3
internal/storage/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
|
||||||
|
package storage
|
||||||
@@ -4,6 +4,20 @@
|
|||||||
-- - utf8mb4 for full Unicode
|
-- - utf8mb4 for full Unicode
|
||||||
-- Booleans are TINYINT(1). Dates use DATE/DATETIME/TIMESTAMP as appropriate.
|
-- Booleans are TINYINT(1). Dates use DATE/DATETIME/TIMESTAMP as appropriate.
|
||||||
|
|
||||||
|
CREATE TABLE audit_registration (
|
||||||
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
username VARCHAR(255) NOT NULL,
|
||||||
|
email VARCHAR(255) NOT NULL,
|
||||||
|
ip VARCHAR(45) NOT NULL,
|
||||||
|
user_agent VARCHAR(500),
|
||||||
|
timestamp DATETIME NOT NULL,
|
||||||
|
INDEX (user_id),
|
||||||
|
CONSTRAINT fk_audit_registration_users
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
-- USERS
|
-- USERS
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
6
internal/storage/migrations/embed.go
Normal file
6
internal/storage/migrations/embed.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import _ "embed"
|
||||||
|
|
||||||
|
//go:embed 0001_initial_create.up.sql
|
||||||
|
var InitialSchema string
|
||||||
6
internal/storage/migrations/read.go
Normal file
6
internal/storage/migrations/read.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
const ProbeUsersTable = `
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = DATABASE() AND table_name = 'users'`
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
securityHelpers "synlotto-website/internal/helpers/security"
|
|
||||||
templateHelpers "synlotto-website/internal/helpers/template"
|
|
||||||
"synlotto-website/internal/logging"
|
|
||||||
|
|
||||||
"synlotto-website/internal/http/middleware"
|
|
||||||
)
|
|
||||||
|
|
||||||
func AdminOnly(db *sql.DB, next http.HandlerFunc) http.HandlerFunc {
|
|
||||||
return middleware.Auth(true)(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
userID, ok := securityHelpers.GetCurrentUserID(r)
|
|
||||||
if !ok || !securityHelpers.IsAdmin(db, userID) {
|
|
||||||
log.Printf("⛔️ Unauthorized admin attempt: user_id=%v, IP=%s, Path=%s", userID, r.RemoteAddr, r.URL.Path)
|
|
||||||
templateHelpers.RenderError(w, r, http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ip := r.RemoteAddr
|
|
||||||
ua := r.UserAgent()
|
|
||||||
path := r.URL.Path
|
|
||||||
|
|
||||||
_, err := db.Exec(`
|
|
||||||
INSERT INTO admin_access_log (user_id, path, ip, user_agent)
|
|
||||||
VALUES (?, ?, ?, ?)`,
|
|
||||||
userID, path, ip, ua,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("⚠️ Failed to log admin access: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("🛡️ Admin access: user_id=%d IP=%s Path=%s", userID, ip, path)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// 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
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
package storage
|
|
||||||
|
|
||||||
const SchemaUsers = `
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
username TEXT NOT NULL UNIQUE,
|
|
||||||
password_hash TEXT NOT NULL,
|
|
||||||
is_admin BOOLEAN
|
|
||||||
);`
|
|
||||||
|
|
||||||
const SchemaThunderballResults = `
|
|
||||||
CREATE TABLE IF NOT EXISTS results_thunderball (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
draw_date TEXT NOT NULL UNIQUE,
|
|
||||||
draw_id INTEGER NOT NULL UNIQUE,
|
|
||||||
machine TEXT,
|
|
||||||
ballset TEXT,
|
|
||||||
ball1 INTEGER,
|
|
||||||
ball2 INTEGER,
|
|
||||||
ball3 INTEGER,
|
|
||||||
ball4 INTEGER,
|
|
||||||
ball5 INTEGER,
|
|
||||||
thunderball INTEGER
|
|
||||||
);`
|
|
||||||
|
|
||||||
const SchemaThunderballPrizes = `
|
|
||||||
CREATE TABLE IF NOT EXISTS prizes_thunderball (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
draw_id INTEGER NOT NULL,
|
|
||||||
draw_date TEXT,
|
|
||||||
prize1 TEXT,
|
|
||||||
prize1_winners INTEGER,
|
|
||||||
prize1_per_winner INTEGER,
|
|
||||||
prize1_fund INTEGER,
|
|
||||||
prize2 TEXT,
|
|
||||||
prize2_winners INTEGER,
|
|
||||||
prize2_per_winner INTEGER,
|
|
||||||
prize2_fund INTEGER,
|
|
||||||
prize3 TEXT,
|
|
||||||
prize3_winners INTEGER,
|
|
||||||
prize3_per_winner INTEGER,
|
|
||||||
prize3_fund INTEGER,
|
|
||||||
prize4 TEXT,
|
|
||||||
prize4_winners INTEGER,
|
|
||||||
prize4_per_winner INTEGER,
|
|
||||||
prize4_fund INTEGER,
|
|
||||||
prize5 TEXT,
|
|
||||||
prize5_winners INTEGER,
|
|
||||||
prize5_per_winner INTEGER,
|
|
||||||
prize5_fund INTEGER,
|
|
||||||
prize6 TEXT,
|
|
||||||
prize6_winners INTEGER,
|
|
||||||
prize6_per_winner INTEGER,
|
|
||||||
prize6_fund INTEGER,
|
|
||||||
prize7 TEXT,
|
|
||||||
prize7_winners INTEGER,
|
|
||||||
prize7_per_winner INTEGER,
|
|
||||||
prize7_fund INTEGER,
|
|
||||||
prize8 TEXT,
|
|
||||||
prize8_winners INTEGER,
|
|
||||||
prize8_per_winner INTEGER,
|
|
||||||
prize8_fund INTEGER,
|
|
||||||
prize9 TEXT,
|
|
||||||
prize9_winners INTEGER,
|
|
||||||
prize9_per_winner INTEGER,
|
|
||||||
prize9_fund INTEGER,
|
|
||||||
total_winners INTEGER,
|
|
||||||
total_prize_fund INTEGER,
|
|
||||||
FOREIGN KEY (draw_date) REFERENCES results_thunderball(draw_date)
|
|
||||||
);`
|
|
||||||
|
|
||||||
const SchemaLottoResults = `
|
|
||||||
CREATE TABLE IF NOT EXISTS results_lotto (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
draw_date TEXT NOT NULL UNIQUE,
|
|
||||||
draw_id INTEGER NOT NULL UNIQUE,
|
|
||||||
machine TEXT,
|
|
||||||
ballset TEXT,
|
|
||||||
ball1 INTEGER,
|
|
||||||
ball2 INTEGER,
|
|
||||||
ball3 INTEGER,
|
|
||||||
ball4 INTEGER,
|
|
||||||
ball5 INTEGER,
|
|
||||||
ball6 INTEGER,
|
|
||||||
bonusball INTEGER
|
|
||||||
);`
|
|
||||||
|
|
||||||
const SchemaMyTickets = `
|
|
||||||
CREATE TABLE IF NOT EXISTS my_tickets (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
userId INTEGER NOT NULL,
|
|
||||||
game_type TEXT NOT NULL,
|
|
||||||
draw_date TEXT NOT NULL,
|
|
||||||
ball1 INTEGER,
|
|
||||||
ball2 INTEGER,
|
|
||||||
ball3 INTEGER,
|
|
||||||
ball4 INTEGER,
|
|
||||||
ball5 INTEGER,
|
|
||||||
ball6 INTEGER,
|
|
||||||
bonus1 INTEGER,
|
|
||||||
bonus2 INTEGER,
|
|
||||||
duplicate BOOLEAN DEFAULT 0,
|
|
||||||
purchase_date TEXT,
|
|
||||||
purchase_method TEXT,
|
|
||||||
image_path TEXT,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
matched_main INTEGER,
|
|
||||||
matched_bonus INTEGER,
|
|
||||||
prize_tier TEXT,
|
|
||||||
is_winner BOOLEAN,
|
|
||||||
prize_amount INTEGER,
|
|
||||||
prize_label TEXT,
|
|
||||||
syndicate_id INTEGER,
|
|
||||||
FOREIGN KEY (userId) REFERENCES users(id)
|
|
||||||
);`
|
|
||||||
|
|
||||||
const SchemaUsersMessages = `
|
|
||||||
CREATE TABLE IF NOT EXISTS users_messages (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
senderId INTEGER NOT NULL REFERENCES users(id),
|
|
||||||
recipientId INTEGER NOT NULL REFERENCES users(id),
|
|
||||||
subject TEXT NOT NULL,
|
|
||||||
message TEXT,
|
|
||||||
is_read BOOLEAN DEFAULT FALSE,
|
|
||||||
is_archived BOOLEAN DEFAULT FALSE,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
archived_at TIMESTAMP
|
|
||||||
);`
|
|
||||||
|
|
||||||
const SchemaUsersNotifications = `
|
|
||||||
CREATE TABLE IF NOT EXISTS users_notification (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
||||||
subject TEXT,
|
|
||||||
body TEXT,
|
|
||||||
is_read BOOLEAN DEFAULT FALSE,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);`
|
|
||||||
|
|
||||||
const SchemaAuditLog = `
|
|
||||||
CREATE TABLE IF NOT EXISTS auditlog (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
username TEXT,
|
|
||||||
success INTEGER,
|
|
||||||
timestamp TEXT
|
|
||||||
);`
|
|
||||||
|
|
||||||
const SchemaLogTicketMatching = `
|
|
||||||
CREATE TABLE IF NOT EXISTS log_ticket_matching (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
triggered_by TEXT,
|
|
||||||
run_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
tickets_matched INTEGER,
|
|
||||||
winners_found INTEGER,
|
|
||||||
notes TEXT
|
|
||||||
);`
|
|
||||||
|
|
||||||
const SchemaAdminAccessLog = `
|
|
||||||
CREATE TABLE IF NOT EXISTS admin_access_log (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id INTEGER,
|
|
||||||
accessed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
path TEXT,
|
|
||||||
ip TEXT,
|
|
||||||
user_agent TEXT
|
|
||||||
);`
|
|
||||||
|
|
||||||
const SchemaNewAuditLog = `
|
|
||||||
CREATE TABLE IF NOT EXISTS audit_log (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id INTEGER,
|
|
||||||
username TEXT,
|
|
||||||
action TEXT,
|
|
||||||
path TEXT,
|
|
||||||
ip TEXT,
|
|
||||||
user_agent TEXT,
|
|
||||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);`
|
|
||||||
|
|
||||||
const SchemaAuditLogin = `
|
|
||||||
CREATE TABLE IF NOT EXISTS audit_login (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
username TEXT,
|
|
||||||
success BOOLEAN,
|
|
||||||
ip TEXT,
|
|
||||||
user_agent TEXT,
|
|
||||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);`
|
|
||||||
|
|
||||||
const SchemaSyndicates = `
|
|
||||||
CREATE TABLE IF NOT EXISTS syndicates (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
owner_id INTEGER NOT NULL,
|
|
||||||
join_code TEXT UNIQUE,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (owner_id) REFERENCES users(id)
|
|
||||||
);`
|
|
||||||
|
|
||||||
const SchemaSyndicateMembers = `
|
|
||||||
CREATE TABLE IF NOT EXISTS syndicate_members (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
syndicate_id INTEGER NOT NULL,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
role TEXT DEFAULT 'member', -- owner, manager, member
|
|
||||||
status TEXT DEFAULT 'active',
|
|
||||||
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (syndicate_id) REFERENCES syndicates(id),
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
||||||
);`
|
|
||||||
|
|
||||||
const SchemaSyndicateInvites = `
|
|
||||||
CREATE TABLE IF NOT EXISTS syndicate_invites (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
syndicate_id INTEGER NOT NULL,
|
|
||||||
invited_user_id INTEGER NOT NULL,
|
|
||||||
sent_by_user_id INTEGER NOT NULL,
|
|
||||||
status TEXT DEFAULT 'pending',
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY(syndicate_id) REFERENCES syndicates(id),
|
|
||||||
FOREIGN KEY(invited_user_id) REFERENCES users(id)
|
|
||||||
);`
|
|
||||||
|
|
||||||
const SchemaSyndicateInviteTokens = `
|
|
||||||
CREATE TABLE IF NOT EXISTS syndicate_invite_tokens (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
syndicate_id INTEGER NOT NULL,
|
|
||||||
token TEXT NOT NULL UNIQUE,
|
|
||||||
invited_by_user_id INTEGER NOT NULL,
|
|
||||||
accepted_by_user_id INTEGER,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
accepted_at TIMESTAMP,
|
|
||||||
expires_at TIMESTAMP,
|
|
||||||
FOREIGN KEY (syndicate_id) REFERENCES syndicates(id),
|
|
||||||
FOREIGN KEY (invited_by_user_id) REFERENCES users(id),
|
|
||||||
FOREIGN KEY (accepted_by_user_id) REFERENCES users(id)
|
|
||||||
);`
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"synlotto-website/internal/logging"
|
|
||||||
"synlotto-website/internal/platform/config"
|
|
||||||
|
|
||||||
// ToDo: remove sqlite
|
|
||||||
_ "modernc.org/sqlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
var db *sql.DB
|
|
||||||
|
|
||||||
func InitDB(filepath string) *sql.DB {
|
|
||||||
var err error
|
|
||||||
cfg := config.Get()
|
|
||||||
db, err = sql.Open("sqlite", filepath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("❌ Failed to open DB:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
schemas := []string{
|
|
||||||
SchemaUsers,
|
|
||||||
SchemaThunderballResults,
|
|
||||||
SchemaThunderballPrizes,
|
|
||||||
SchemaLottoResults,
|
|
||||||
SchemaMyTickets,
|
|
||||||
SchemaUsersMessages,
|
|
||||||
SchemaUsersNotifications,
|
|
||||||
SchemaAuditLog,
|
|
||||||
SchemaAuditLogin,
|
|
||||||
SchemaLogTicketMatching,
|
|
||||||
SchemaAdminAccessLog,
|
|
||||||
SchemaNewAuditLog,
|
|
||||||
SchemaSyndicates,
|
|
||||||
SchemaSyndicateMembers,
|
|
||||||
SchemaSyndicateInvites,
|
|
||||||
SchemaSyndicateInviteTokens,
|
|
||||||
}
|
|
||||||
if cfg == nil {
|
|
||||||
logging.Error("❌ config is nil — did config.Init() run before InitDB?")
|
|
||||||
panic("config not ready")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, stmt := range schemas {
|
|
||||||
if _, err := db.Exec(stmt); err != nil {
|
|
||||||
log.Fatalf("❌ Failed to apply schema: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"synlotto-website/internal/logging"
|
|
||||||
"synlotto-website/internal/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetUserByID(db *sql.DB, id int) *models.User {
|
|
||||||
row := db.QueryRow("SELECT id, username, password_hash, is_admin FROM users WHERE id = ?", id)
|
|
||||||
|
|
||||||
var user models.User
|
|
||||||
err := row.Scan(&user.Id, &user.Username, &user.PasswordHash, &user.IsAdmin)
|
|
||||||
if err != nil {
|
|
||||||
if err != sql.ErrNoRows {
|
|
||||||
logging.Error("DB error:", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &user
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetUserByUsername(db *sql.DB, username string) *models.User {
|
|
||||||
row := db.QueryRow(`SELECT id, username, password_hash, is_admin FROM users WHERE username = ?`, username)
|
|
||||||
|
|
||||||
var u models.User
|
|
||||||
err := row.Scan(&u.Id, &u.Username, &u.PasswordHash, &u.IsAdmin)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &u
|
|
||||||
}
|
|
||||||
@@ -130,6 +130,35 @@ func GetSyndicateMembers(db *sql.DB, syndicateID int) []models.SyndicateMember {
|
|||||||
return members
|
return members
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
func IsSyndicateManager(db *sql.DB, syndicateID, userID int) bool {
|
func IsSyndicateManager(db *sql.DB, syndicateID, userID int) bool {
|
||||||
var count int
|
var count int
|
||||||
err := db.QueryRow(`
|
err := db.QueryRow(`
|
||||||
@@ -3,41 +3,19 @@ package storage
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"synlotto-website/internal/models"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// todo should be a ticket function?
|
func UpdateInviteStatus(db *sql.DB, inviteID int, status string) error {
|
||||||
func GetSyndicateTickets(db *sql.DB, syndicateID int) []models.Ticket {
|
_, err := db.Exec(`
|
||||||
rows, err := db.Query(`
|
UPDATE syndicate_invites
|
||||||
SELECT id, userId, syndicateId, game_type, draw_date, ball1, ball2, ball3, ball4, ball5, ball6,
|
SET status = ?
|
||||||
bonus1, bonus2, matched_main, matched_bonus, prize_tier, prize_amount, prize_label, is_winner
|
WHERE id = ?
|
||||||
FROM my_tickets
|
`, status, inviteID)
|
||||||
WHERE syndicateId = ?
|
return err
|
||||||
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
|
// ToDo: both a read and inset break up
|
||||||
func AcceptInvite(db *sql.DB, inviteID, userID int) error {
|
func AcceptInvite(db *sql.DB, inviteID, userID int) error {
|
||||||
var syndicateID int
|
var syndicateID int
|
||||||
err := db.QueryRow(`
|
err := db.QueryRow(`
|
||||||
27
internal/storage/users/create.go
Normal file
27
internal/storage/users/create.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package usersStorage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CreateUserSQL = `
|
||||||
|
INSERT INTO users (username, email, password_hash, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())`
|
||||||
|
|
||||||
|
func CreateUser(db *sql.DB, username, email, passwordHash string) (int64, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
res, err := db.ExecContext(ctx, CreateUserSQL, username, email, passwordHash)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
id, err := res.LastInsertId()
|
||||||
|
if err != nil || id == 0 {
|
||||||
|
return 0, errors.New("could not get insert id")
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
72
internal/storage/users/read.go
Normal file
72
internal/storage/users/read.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package usersStorage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"synlotto-website/internal/logging"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
UsernameExistsSQL = `
|
||||||
|
SELECT EXISTS(SELECT 1 FROM users WHERE username = ? LIMIT 1)`
|
||||||
|
|
||||||
|
EmailExistsSQL = `
|
||||||
|
SELECT EXISTS(SELECT 1 FROM users WHERE email = ? LIMIT 1)`
|
||||||
|
|
||||||
|
GetByUsernameSQL = `
|
||||||
|
SELECT id, username, email, password_hash, created_at, updated_at
|
||||||
|
FROM users
|
||||||
|
WHERE username = ?
|
||||||
|
LIMIT 1`
|
||||||
|
|
||||||
|
GetByIDSQL = `
|
||||||
|
SELECT id, username, email, password_hash, is_admin, created_at, updated_at
|
||||||
|
FROM users
|
||||||
|
WHERE id = ?
|
||||||
|
LIMIT 1`
|
||||||
|
)
|
||||||
|
|
||||||
|
func UsernameExists(db *sql.DB, username string) bool {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
var exists bool
|
||||||
|
_ = db.QueryRowContext(ctx, UsernameExistsSQL, username).Scan(&exists)
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
func EmailExists(db *sql.DB, email string) bool {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
var exists bool
|
||||||
|
_ = db.QueryRowContext(ctx, EmailExistsSQL, email).Scan(&exists)
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserByUsername(db *sql.DB, username string) *User {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
var u User
|
||||||
|
err := db.QueryRowContext(ctx, GetByUsernameSQL, username).
|
||||||
|
Scan(&u.Id, &u.Username, &u.Email, &u.PasswordHash, &u.CreatedAt, &u.UpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &u
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserByID(db *sql.DB, id int) *User {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var u User
|
||||||
|
err := db.QueryRowContext(ctx, GetByIDSQL, id).
|
||||||
|
Scan(&u.Id, &u.Username, &u.Email, &u.PasswordHash, &u.IsAdmin, &u.CreatedAt, &u.UpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
logging.Error("GetUserByID: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &u
|
||||||
|
}
|
||||||
5
internal/storage/users/types.go
Normal file
5
internal/storage/users/types.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package usersStorage
|
||||||
|
|
||||||
|
import "synlotto-website/internal/models"
|
||||||
|
|
||||||
|
type User = models.User
|
||||||
88
main.go
88
main.go
@@ -1,88 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"synlotto-website/bootstrap"
|
|
||||||
"synlotto-website/config"
|
|
||||||
"synlotto-website/handlers"
|
|
||||||
"synlotto-website/logging"
|
|
||||||
"synlotto-website/middleware"
|
|
||||||
"synlotto-website/models"
|
|
||||||
"synlotto-website/routes"
|
|
||||||
"synlotto-website/storage"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
appState, err := bootstrap.LoadAppState("config/config.json")
|
|
||||||
if err != nil {
|
|
||||||
logging.Error("Failed to load app state: %v", err)
|
|
||||||
}
|
|
||||||
config.Init(appState.Config)
|
|
||||||
logging.LogConfig(appState.Config)
|
|
||||||
|
|
||||||
db := storage.InitDB("synlotto.db")
|
|
||||||
models.SetDB(db) // ToDo: Should be in storage not models.
|
|
||||||
|
|
||||||
err = bootstrap.InitSession(appState.Config)
|
|
||||||
if err != nil {
|
|
||||||
logging.Error("❌ Failed to init session: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToDo: if err := bootstrap.InitLicenseChecker(appState.Config); err != nil {
|
|
||||||
// logging.Error("❌ Invalid license: %v", err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
err = bootstrap.InitCSRFProtection([]byte(appState.Config.CSRF.CSRFKey), appState.Config.HttpServer.ProductionMode)
|
|
||||||
if err != nil {
|
|
||||||
logging.Error("Failed to init CSRF: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
routes.SetupAdminRoutes(mux, db)
|
|
||||||
routes.SetupAccountRoutes(mux, db)
|
|
||||||
routes.SetupResultRoutes(mux, db)
|
|
||||||
routes.SetupSyndicateRoutes(mux, db)
|
|
||||||
routes.SetupStatisticsRoutes(mux, db)
|
|
||||||
|
|
||||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
|
||||||
mux.HandleFunc("/", handlers.Home(db))
|
|
||||||
|
|
||||||
wrapped := bootstrap.CSRFMiddleware(mux)
|
|
||||||
wrapped = middleware.RateLimit(wrapped)
|
|
||||||
wrapped = middleware.EnforceHTTPS(wrapped, appState.Config.HttpServer.ProductionMode)
|
|
||||||
wrapped = middleware.SecureHeaders(wrapped)
|
|
||||||
wrapped = middleware.Recover(wrapped)
|
|
||||||
|
|
||||||
addr := fmt.Sprintf("%s:%d", appState.Config.HttpServer.Address, appState.Config.HttpServer.Port)
|
|
||||||
srv := &http.Server{
|
|
||||||
Addr: addr,
|
|
||||||
Handler: wrapped,
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
logging.Info("Server running at %s\n", addr)
|
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
||||||
logging.Error("Server error: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
quit := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(quit, os.Interrupt)
|
|
||||||
<-quit
|
|
||||||
logging.Info("Shutting down server...")
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := srv.Shutdown(ctx); err != nil {
|
|
||||||
logging.Error("Forced shutdown: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logging.Info("Server shutdown complete")
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<h2>Login</h2>
|
<h2>Login</h2>
|
||||||
<form method="POST" action="/account/login" class="form">
|
<form method="POST" action="/account/login" class="form">
|
||||||
{{ .CSRFField }}
|
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="username">Username:</label>
|
<label for="username">Username:</label>
|
||||||
|
|||||||
@@ -1,9 +1,43 @@
|
|||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<h2>Sign Up</h2>
|
<h2>Create your account</h2>
|
||||||
<form method="POST" action="/account/signup">
|
{{ if .Flash }}<div class="flash">{{ .Flash }}</div>{{ end }}
|
||||||
{{ .csrfField }}
|
|
||||||
<label>Username: <input type="text" name="username" required></label><br>
|
<form method="POST" action="/account/signup" class="form">
|
||||||
<label>Password: <input type="password" name="password" required></label><br>
|
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
|
||||||
<button type="submit">Sign Up</button>
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" name="username" id="username" required class="form-control"
|
||||||
|
value="{{ with .Form }}{{ .Username }}{{ end }}">
|
||||||
|
{{ with .Errors }}{{ with index . "username" }}<div class="error">{{ . }}</div>{{ end }}{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" name="email" id="email" required class="form-control"
|
||||||
|
value="{{ with .Form }}{{ .Email }}{{ end }}">
|
||||||
|
{{ with .Errors }}{{ with index . "email" }}<div class="error">{{ . }}</div>{{ end }}{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" name="password" id="password" required class="form-control">
|
||||||
|
{{ with .Errors }}{{ with index . "password" }}<div class="error">{{ . }}</div>{{ end }}{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password_confirm">Confirm Password</label>
|
||||||
|
<input type="password" name="password_confirm" id="password_confirm" required class="form-control">
|
||||||
|
{{ with .Errors }}{{ with index . "password_confirm" }}<div class="error">{{ . }}</div>{{ end }}{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input type="checkbox" name="accept_terms" id="accept_terms" class="form-check-input"
|
||||||
|
{{ with .Form }}{{ if .AcceptTerms }}checked{{ end }}{{ end }}>
|
||||||
|
<label for="accept_terms" class="form-check-label">I accept the terms</label>
|
||||||
|
{{ with .Errors }}{{ with index . "accept_terms" }}<div class="error">{{ . }}</div>{{ end }}{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">Create account</button>
|
||||||
</form>
|
</form>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user