diff --git a/cmd/api/main.go b/cmd/api/main.go new file mode 100644 index 0000000..f35d7b6 --- /dev/null +++ b/cmd/api/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + middleware "synlotto-website/internal/http/middleware" + + "synlotto-website/internal/platform/config" + "synlotto-website/internal/platform/csrf" + "synlotto-website/internal/platform/session" + + "github.com/gin-gonic/gin" +) + +func main() { + cfg, err := config.Load("config.json") + if err != nil { + panic(fmt.Errorf("load config: %w", err)) + } + + sessions := session.New(cfg) + + router := gin.New() + router.Use(gin.Logger(), gin.Recovery()) + router.Use(middleware.Session(sessions)) + + router.GET("/healthz", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + }) + + handler := csrf.Wrap(router, cfg) + + addr := fmt.Sprintf("%s:%d", cfg.HttpServer.Address, cfg.HttpServer.Port) + srv := &http.Server{ + Addr: addr, + Handler: handler, + ReadHeaderTimeout: 10 * time.Second, + } + + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + panic(err) + } + }() + fmt.Printf("Server running on http://%s\n", 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) +} diff --git a/go.mod b/go.mod index fcb49b1..f966c15 100644 --- a/go.mod +++ b/go.mod @@ -3,27 +3,55 @@ module synlotto-website go 1.24.1 require ( - github.com/gorilla/csrf v1.7.2 - github.com/gorilla/sessions v1.4.0 - golang.org/x/crypto v0.36.0 + github.com/alexedwards/scs/v2 v2.9.0 + github.com/gin-gonic/gin v1.11.0 + github.com/go-sql-driver/mysql v1.9.3 + github.com/golang-migrate/migrate/v4 v4.19.0 + github.com/justinas/nosurf v1.2.0 + golang.org/x/crypto v0.40.0 golang.org/x/time v0.11.0 modernc.org/sqlite v1.36.1 ) require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/go-sql-driver/mysql v1.9.3 // indirect - github.com/golang-migrate/migrate/v4 v4.19.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.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/google/uuid v1.6.0 // indirect - github.com/gorilla/securecookie v1.1.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // 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/exp v0.0.0-20230315142452-642cacee5cc0 // indirect - golang.org/x/sys v0.31.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 modernc.org/libc v1.61.13 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.8.2 // indirect diff --git a/go.sum b/go.sum index 759e675..0691f0f 100644 --- a/go.sum +++ b/go.sum @@ -1,51 +1,172 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90= +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/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +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/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= +github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +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-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +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/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE= github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI= -github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk= -github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= -github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= -github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= -github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/justinas/nosurf v1.2.0 h1:yMs1bSRrNiwXk4AS6n8vL2Ssgpb9CB25T/4xrixaK0s= +github.com/justinas/nosurf v1.2.0/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 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/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +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.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +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/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= -golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +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/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= -golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo= diff --git a/internal/handlers/account/authentication.go b/internal/handlers/account/authentication.go index bbf0e28..fa97ba7 100644 --- a/internal/handlers/account/authentication.go +++ b/internal/handlers/account/authentication.go @@ -9,12 +9,13 @@ import ( 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" + auditlogStorage "synlotto-website/internal/storage/auditlog" + usersStorage "synlotto-website/internal/storage/users" "github.com/gorilla/csrf" + "github.com/justinas/nosurf" ) func Login(db *sql.DB) http.HandlerFunc { @@ -29,7 +30,7 @@ func Login(db *sql.DB) http.HandlerFunc { tmpl := templateHelpers.LoadTemplateFiles("login.html", "templates/account/login.html") data := models.TemplateData{} context := templateHelpers.TemplateContext(w, r, data) - context["csrfField"] = csrf.TemplateField(r) + context["CSRFToken"] = nosurf.Token(r) if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil { logging.Info("❌ Template render error:", err) @@ -44,10 +45,10 @@ func Login(db *sql.DB) http.HandlerFunc { // 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) + user := usersStorage.GetUserByUsername(db, username) if user == nil { logging.Info("❌ User not found: %s", username) - storage.LogLoginAttempt(r, username, false) + auditlogStorage.LogLoginAttempt(db, r, username, false) session, _ := httpHelpers.GetSession(w, r) session.Values["flash"] = "Invalid username or password." @@ -58,7 +59,7 @@ func Login(db *sql.DB) http.HandlerFunc { if !securityHelpers.CheckPasswordHash(user.PasswordHash, password) { logging.Info("❌ Password mismatch for user: %s", username) - storage.LogLoginAttempt(r, username, false) + auditlogStorage.LogLoginAttempt(db, r, username, false) session, _ := httpHelpers.GetSession(w, r) session.Values["flash"] = "Invalid username or password." @@ -69,7 +70,7 @@ func Login(db *sql.DB) http.HandlerFunc { } logging.Info("βœ… Login successful for user: %s", username) - storage.LogLoginAttempt(r, username, true) + auditlogStorage.LogLoginAttempt(db, r, username, true) session, _ := httpHelpers.GetSession(w, r) for k := range session.Values { @@ -112,30 +113,39 @@ func Logout(w http.ResponseWriter, r *http.Request) { 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") +// ToDo: opted to inject the repo which is better for tests/DI rather than taking the *sql.DB +func Signup(usersRepo *usersStorage.UsersRepo) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + tmpl := templateHelpers.LoadTemplateFiles("signup.html", "templates/account/signup.html") + if err := tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ + "csrfField": csrf.TemplateField(r), // ToDo: this is the olf Gorilla thing + }); err != nil { + logging.Info("❌ Template render error: %v", err) + http.Error(w, "Error rendering signup page", http.StatusInternalServerError) + } + return + } - tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ - "csrfField": csrf.TemplateField(r), - }) - return + username := r.FormValue("username") + password := r.FormValue("password") + + if username == "" || password == "" { + http.Error(w, "Username and password are required", http.StatusBadRequest) + return + } + + hashed, err := securityHelpers.HashPassword(password) + if err != nil { + http.Error(w, "Server error", http.StatusInternalServerError) + return + } + + if err := usersRepo.Create(r.Context(), username, hashed, false); err != nil { + http.Error(w, "Could not create user", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/account/login", http.StatusSeeOther) } - - 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) } diff --git a/internal/handlers/admin/dashboard.go b/internal/handlers/admin/dashboard.go index d5a1031..bddbf1b 100644 --- a/internal/handlers/admin/dashboard.go +++ b/internal/handlers/admin/dashboard.go @@ -10,7 +10,7 @@ import ( templateHelpers "synlotto-website/internal/helpers/template" "synlotto-website/internal/models" - "synlotto-website/internal/storage" + usersStorage "synlotto-website/internal/storage/users" ) var ( @@ -26,7 +26,7 @@ func AdminDashboardHandler(db *sql.DB) http.HandlerFunc { return } - user := storage.GetUserByID(db, userID) + user := usersStorage.GetUserByID(db, userID) if user == nil { http.Error(w, "User not found", http.StatusUnauthorized) return @@ -36,7 +36,7 @@ func AdminDashboardHandler(db *sql.DB) http.HandlerFunc { context := templateHelpers.TemplateContext(w, r, data) context["User"] = user context["IsAdmin"] = user.IsAdmin - // Missing messages, notifications, potentially syndicate notifictions if that becomes a new top bar icon. + // ToDo: Missing messages, notifications, potentially syndicate notifictions if that becomes a new top bar icon. db.QueryRow(`SELECT COUNT(*), SUM(CASE WHEN is_winner THEN 1 ELSE 0 END), SUM(prize_amount) FROM my_tickets`).Scan(&total, &winners, &prizeSum) context["Stats"] = map[string]interface{}{ "TotalTickets": total, diff --git a/internal/handlers/lottery/draws/draw_handler.go b/internal/handlers/lottery/draws/draw_handler.go index 988ac57..22c6e56 100644 --- a/internal/handlers/lottery/draws/draw_handler.go +++ b/internal/handlers/lottery/draws/draw_handler.go @@ -5,11 +5,10 @@ import ( "log" "net/http" - templateHelpers "synlotto-website/internal/helpers/template" - "synlotto-website/internal/helpers" + templateHelpers "synlotto-website/internal/helpers/template" "synlotto-website/internal/models" - "synlotto-website/internal/storage" + resultsThunderballStorage "synlotto-website/internal/storage/results/thunderball" ) func NewDraw(db *sql.DB) http.HandlerFunc { @@ -45,7 +44,7 @@ func Submit(db *sql.DB, w http.ResponseWriter, r *http.Request) { Thunderball: helpers.Atoi(r.FormValue("thunderball")), } - err := storage.InsertThunderballResult(db, draw) + err := resultsThunderballStorage.InsertThunderballResult(db, draw) if err != nil { log.Println("❌ Failed to insert draw:", err) http.Error(w, "Failed to save draw", http.StatusInternalServerError) diff --git a/internal/handlers/lottery/syndicate/syndicate.go b/internal/handlers/lottery/syndicate/syndicate.go index a990ae0..caa6b6a 100644 --- a/internal/handlers/lottery/syndicate/syndicate.go +++ b/internal/handlers/lottery/syndicate/syndicate.go @@ -9,10 +9,11 @@ import ( templateHandlers "synlotto-website/internal/handlers/template" securityHelpers "synlotto-website/internal/helpers/security" 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/models" - "synlotto-website/internal/storage" ) func CreateSyndicateHandler(db *sql.DB) http.HandlerFunc { @@ -35,7 +36,7 @@ func CreateSyndicateHandler(db *sql.DB) http.HandlerFunc { return } - _, err := storage.CreateSyndicate(db, userId, name, description) + _, err := syndicateStorage.CreateSyndicate(db, userId, name, description) if err != nil { log.Printf("❌ CreateSyndicate failed: %v", err) templateHelpers.SetFlash(w, r, "Failed to create syndicate") @@ -58,8 +59,8 @@ func ListSyndicatesHandler(db *sql.DB) http.HandlerFunc { return } - managed := storage.GetSyndicatesByOwner(db, userID) - member := storage.GetSyndicatesByMember(db, userID) + managed := syndicateStorage.GetSyndicatesByOwner(db, userID) + member := syndicateStorage.GetSyndicatesByMember(db, userID) managedMap := make(map[int]bool) for _, s := range managed { @@ -92,21 +93,21 @@ func ViewSyndicateHandler(db *sql.DB) http.HandlerFunc { } syndicateID := helpers.Atoi(r.URL.Query().Get("id")) - syndicate, err := storage.GetSyndicateByID(db, syndicateID) + syndicate, err := syndicateStorage.GetSyndicateByID(db, syndicateID) if err != nil || syndicate == nil { templateHelpers.RenderError(w, r, 404) return } isManager := userID == syndicate.OwnerID - isMember := storage.IsSyndicateMember(db, syndicateID, userID) + isMember := syndicateStorage.IsSyndicateMember(db, syndicateID, userID) if !isManager && !isMember { templateHelpers.RenderError(w, r, 403) return } - members := storage.GetSyndicateMembers(db, syndicateID) + members := syndicateStorage.GetSyndicateMembers(db, syndicateID) data := templateHandlers.BuildTemplateData(db, w, r) context := templateHelpers.TemplateContext(w, r, data) @@ -128,7 +129,7 @@ func SyndicateLogTicketHandler(db *sql.DB) http.HandlerFunc { } syndicateId := helpers.Atoi(r.URL.Query().Get("id")) - syndicate, err := storage.GetSyndicateByID(db, syndicateId) + syndicate, err := syndicateStorage.GetSyndicateByID(db, syndicateId) if err != nil || syndicate.OwnerID != userID { templateHelpers.RenderError(w, r, 403) return @@ -148,7 +149,7 @@ func SyndicateLogTicketHandler(db *sql.DB) http.HandlerFunc { drawDate := r.FormValue("draw_date") method := r.FormValue("purchase_method") - err := storage.InsertTicket(db, models.Ticket{ + err := ticketStorage.InsertTicket(db, models.Ticket{ UserId: userID, GameType: gameType, DrawDate: drawDate, @@ -185,12 +186,12 @@ func SyndicateTicketsHandler(db *sql.DB) http.HandlerFunc { return } - if !storage.IsSyndicateMember(db, syndicateID, userID) { + if !syndicateStorage.IsSyndicateMember(db, syndicateID, userID) { templateHelpers.RenderError(w, r, 403) return } - tickets := storage.GetSyndicateTickets(db, syndicateID) + tickets := ticketStorage.GetSyndicateTickets(db, syndicateID) data := templateHandlers.BuildTemplateData(db, w, r) context := templateHelpers.TemplateContext(w, r, data) diff --git a/internal/handlers/lottery/syndicate/syndicate_invites.go b/internal/handlers/lottery/syndicate/syndicate_invites.go index d43cffc..b812c68 100644 --- a/internal/handlers/lottery/syndicate/syndicate_invites.go +++ b/internal/handlers/lottery/syndicate/syndicate_invites.go @@ -10,16 +10,17 @@ import ( templateHandlers "synlotto-website/internal/handlers/template" securityHelpers "synlotto-website/internal/helpers/security" templateHelpers "synlotto-website/internal/helpers/template" + storage "synlotto-website/internal/storage/syndicate" + syndicateStorage "synlotto-website/internal/storage/syndicate" "synlotto-website/internal/helpers" - "synlotto-website/internal/storage" ) func SyndicateInviteHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { - templateHelpers.RenderError(w, r, http.StatusForbidden) + templateHandlers.RenderError(w, r, http.StatusForbidden) return } @@ -33,13 +34,13 @@ func SyndicateInviteHandler(db *sql.DB) http.HandlerFunc { tmpl := templateHelpers.LoadTemplateFiles("invite-syndicate.html", "templates/syndicate/invite.html") err := tmpl.ExecuteTemplate(w, "layout", context) if err != nil { - templateHelpers.RenderError(w, r, 500) + templateHandlers.RenderError(w, r, 500) } case http.MethodPost: syndicateID := helpers.Atoi(r.FormValue("syndicate_id")) username := r.FormValue("username") - err := storage.InviteToSyndicate(db, userID, syndicateID, username) + err := syndicateStorage.InviteToSyndicate(db, userID, syndicateID, username) if err != nil { templateHelpers.SetFlash(w, r, "Failed to send invite: "+err.Error()) } else { @@ -48,7 +49,7 @@ func SyndicateInviteHandler(db *sql.DB) http.HandlerFunc { http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther) default: - templateHelpers.RenderError(w, r, http.StatusMethodNotAllowed) + templateHandlers.RenderError(w, r, http.StatusMethodNotAllowed) } } } @@ -57,11 +58,11 @@ func ViewInvitesHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { - templateHelpers.RenderError(w, r, 403) + templateHandlers.RenderError(w, r, 403) return } - invites := storage.GetPendingInvites(db, userID) + invites := syndicateStorage.GetPendingInvites(db, userID) data := templateHandlers.BuildTemplateData(db, w, r) context := templateHelpers.TemplateContext(w, r, data) context["Invites"] = invites @@ -76,10 +77,10 @@ func AcceptInviteHandler(db *sql.DB) http.HandlerFunc { inviteID := helpers.Atoi(r.URL.Query().Get("id")) userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { - templateHelpers.RenderError(w, r, 403) + templateHandlers.RenderError(w, r, 403) return } - err := storage.AcceptInvite(db, inviteID, userID) + err := syndicateStorage.AcceptInvite(db, inviteID, userID) if err != nil { templateHelpers.SetFlash(w, r, "Failed to accept invite") } else { @@ -92,7 +93,7 @@ func AcceptInviteHandler(db *sql.DB) http.HandlerFunc { func DeclineInviteHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { inviteID := helpers.Atoi(r.URL.Query().Get("id")) - _ = storage.UpdateInviteStatus(db, inviteID, "declined") + _ = syndicateStorage.UpdateInviteStatus(db, inviteID, "declined") http.Redirect(w, r, "/syndicate/invites", http.StatusSeeOther) } } @@ -113,6 +114,7 @@ func CreateInviteToken(db *sql.DB, syndicateID, invitedByID int, ttlHours int) ( return token, err } +// ToDo: Whys is there SQL in here??? Shouldn't be in handlers func AcceptInviteToken(db *sql.DB, token string, userID int) error { var syndicateID int var expiresAt, acceptedAt sql.NullTime diff --git a/internal/handlers/lottery/tickets/ticket_handler.go b/internal/handlers/lottery/tickets/ticket_handler.go index 4c13ef3..3e24427 100644 --- a/internal/handlers/lottery/tickets/ticket_handler.go +++ b/internal/handlers/lottery/tickets/ticket_handler.go @@ -18,7 +18,7 @@ import ( "synlotto-website/internal/helpers" "synlotto-website/internal/models" - "github.com/gorilla/csrf" + "github.com/justinas/nosurf" ) func AddTicket(db *sql.DB) http.HandlerFunc { @@ -46,7 +46,7 @@ func AddTicket(db *sql.DB) http.HandlerFunc { data := models.TemplateData{} context := templateHelpers.TemplateContext(w, r, data) - context["csrfField"] = csrf.TemplateField(r) + context["CSRFToken"] = nosurf.Token(r) context["DrawDates"] = drawDates tmpl := templateHelpers.LoadTemplateFiles("add_ticket.html", "templates/account/tickets/add_ticket.html") diff --git a/internal/handlers/messages.go b/internal/handlers/messages.go index 67a7134..b294526 100644 --- a/internal/handlers/messages.go +++ b/internal/handlers/messages.go @@ -8,10 +8,13 @@ import ( templateHandlers "synlotto-website/internal/handlers/template" httpHelpers "synlotto-website/internal/helpers/http" securityHelpers "synlotto-website/internal/helpers/security" + + // ToDo multi storage references need handler? templateHelpers "synlotto-website/internal/helpers/template" + messagesStorage "synlotto-website/internal/storage/messages" + storage "synlotto-website/internal/storage/messages" "synlotto-website/internal/helpers" - storage "synlotto-website/internal/storage/mysql" ) func MessagesInboxHandler(db *sql.DB) http.HandlerFunc { @@ -28,13 +31,13 @@ func MessagesInboxHandler(db *sql.DB) http.HandlerFunc { } perPage := 10 - totalCount := storage.GetInboxMessageCount(db, userID) + totalCount := messagesStorage.GetInboxMessageCount(db, userID) totalPages := (totalCount + perPage - 1) / perPage if totalPages == 0 { totalPages = 1 } - messages := storage.GetInboxMessages(db, userID, page, perPage) + messages := messagesStorage.GetInboxMessages(db, userID, page, perPage) data := templateHandlers.BuildTemplateData(db, w, r) context := templateHelpers.TemplateContext(w, r, data) @@ -92,7 +95,7 @@ func ArchiveMessageHandler(db *sql.DB) http.HandlerFunc { return } - err := storage.ArchiveMessage(db, userID, id) + err := messagesStorage.ArchiveMessage(db, userID, id) if err != nil { templateHelpers.SetFlash(w, r, "Failed to archive message.") } else { @@ -117,7 +120,7 @@ func ArchivedMessagesHandler(db *sql.DB) http.HandlerFunc { } perPage := 10 - messages := storage.GetArchivedMessages(db, userID, page, perPage) + messages := messagesStorage.GetArchivedMessages(db, userID, page, perPage) hasMore := len(messages) == perPage data := templateHandlers.BuildTemplateData(db, w, r) @@ -153,7 +156,7 @@ func SendMessageHandler(db *sql.DB) http.HandlerFunc { subject := r.FormValue("subject") body := r.FormValue("message") - if err := storage.SendMessage(db, senderID, recipientID, subject, body); err != nil { + if err := messagesStorage.SendMessage(db, senderID, recipientID, subject, body); err != nil { templateHelpers.SetFlash(w, r, "Failed to send message.") } else { templateHelpers.SetFlash(w, r, "Message sent.") diff --git a/internal/handlers/notifications.go b/internal/handlers/notifications.go index 80369a2..adcc2af 100644 --- a/internal/handlers/notifications.go +++ b/internal/handlers/notifications.go @@ -9,8 +9,7 @@ import ( templateHandlers "synlotto-website/internal/handlers/template" httpHelpers "synlotto-website/internal/helpers/http" templateHelpers "synlotto-website/internal/helpers/template" - - "synlotto-website/internal/storage" + notificationsStorage "synlotto-website/internal/storage/notifications" ) func NotificationsHandler(db *sql.DB) http.HandlerFunc { @@ -44,12 +43,12 @@ func MarkNotificationReadHandler(db *sql.DB) http.HandlerFunc { return } - notification, err := storage.GetNotificationByID(db, userID, notificationID) + notification, err := notificationsStorage.GetNotificationByID(db, userID, notificationID) if err != nil { log.Printf("❌ Notification not found or belongs to another user: %v", err) notification = nil } else if !notification.IsRead { - err = storage.MarkNotificationAsRead(db, userID, notificationID) + err = notificationsStorage.MarkNotificationAsRead(db, userID, notificationID) if err != nil { log.Printf("⚠️ Failed to mark as read: %v", err) } diff --git a/internal/handlers/session/account.go b/internal/handlers/session/account.go deleted file mode 100644 index 06eb2be..0000000 --- a/internal/handlers/session/account.go +++ /dev/null @@ -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) -} diff --git a/internal/handlers/session/auth.go b/internal/handlers/session/auth.go deleted file mode 100644 index 6da5493..0000000 --- a/internal/handlers/session/auth.go +++ /dev/null @@ -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 -} diff --git a/internal/handlers/template/error.go b/internal/handlers/template/error.go new file mode 100644 index 0000000..d58b643 --- /dev/null +++ b/internal/handlers/template/error.go @@ -0,0 +1,63 @@ +package handlers + +// 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. + +import ( + "fmt" + "log" + "net/http" + "os" + + templateHelpers "synlotto-website/internal/helpers/template" + + "synlotto-website/internal/models" +) + +func RenderError( + w http.ResponseWriter, + r *http.Request, + statusCode int, + siteName string, + copyrightYearStart int, +) { + log.Printf("βš™οΈ RenderError called with status: %d", statusCode) + + context := templateHelpers.TemplateContext( + w, r, + models.TemplateData{}, + siteName, + copyrightYearStart, + ) + + pagePath := fmt.Sprintf("templates/error/%d.html", statusCode) + log.Printf("πŸ“„ Checking for template file: %s", pagePath) + + if _, err := os.Stat(pagePath); err != nil { + log.Printf("🚫 Template file missing: %s", err) + http.Error(w, http.StatusText(statusCode), statusCode) + return + } + + log.Println("βœ… Template file found, loading...") + + tmpl := templateHelpers.LoadTemplateFiles(fmt.Sprintf("%d.html", statusCode), pagePath) + + w.WriteHeader(statusCode) + if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil { + log.Printf("❌ Failed to render error page layout: %v", err) + http.Error(w, http.StatusText(statusCode), statusCode) + return + } + + log.Println("βœ… Successfully rendered error page") // ToDo: log these to db +} diff --git a/internal/handlers/template/render.go b/internal/handlers/template/render.go new file mode 100644 index 0000000..b3d115f --- /dev/null +++ b/internal/handlers/template/render.go @@ -0,0 +1,11 @@ +package handlers + +import "synlotto-website/internal/platform/config" + +type Handler struct { + cfg config.Config +} + +func New(cfg config.Config) *Handler { + return &Handler{cfg: cfg} +} diff --git a/internal/handlers/template/templatedata.go b/internal/handlers/template/templatedata.go index e504e3d..8fc5d39 100644 --- a/internal/handlers/template/templatedata.go +++ b/internal/handlers/template/templatedata.go @@ -6,9 +6,12 @@ import ( "net/http" httpHelper "synlotto-website/internal/helpers/http" + // ToDo: again, need to check if i should be using multiple stotage entries like this or if this si even correct would it not be a helper? + 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/storage" ) func BuildTemplateData(db *sql.DB, w http.ResponseWriter, r *http.Request) models.TemplateData { @@ -25,13 +28,13 @@ func BuildTemplateData(db *sql.DB, w http.ResponseWriter, r *http.Request) model var messages []models.Message if userId, ok := session.Values["user_id"].(int); ok { - user = storage.GetUserByID(db, userId) + user = usersStorage.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) + notificationCount = notificationStorage.GetNotificationCount(db, user.Id) + notifications = notificationStorage.GetRecentNotifications(db, user.Id, 15) + messageCount, _ = messageStorage.GetMessageCount(db, user.Id) + messages = messageStorage.GetRecentMessages(db, user.Id, 15) } } diff --git a/internal/helpers/template/build.go b/internal/helpers/template/build.go index e1f80b5..67a0c82 100644 --- a/internal/helpers/template/build.go +++ b/internal/helpers/template/build.go @@ -2,24 +2,33 @@ package helpers import ( "html/template" - "log" "net/http" "strings" "time" - helpers "synlotto-website/internal/helpers/http" + httpHelpers "synlotto-website/internal/helpers/http" "synlotto-website/internal/models" - "synlotto-website/internal/platform/config" - "github.com/gorilla/csrf" + "github.com/justinas/nosurf" ) -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!") +// 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, } - session, _ := helpers.GetSession(w, r) +} + +func TemplateContext(w http.ResponseWriter, r *http.Request, data models.TemplateData) map[string]interface{} { + session, _ := httpHelpers.GetSession(w, r) var flash string if f, ok := session.Values["flash"].(string); ok { @@ -29,7 +38,7 @@ func TemplateContext(w http.ResponseWriter, r *http.Request, data models.Templat } return map[string]interface{}{ - "CSRFField": csrf.TemplateField(r), + "CSRFToken": nosurf.Token(r), "Flash": flash, "User": data.User, "IsAdmin": data.IsAdmin, @@ -37,8 +46,8 @@ func TemplateContext(w http.ResponseWriter, r *http.Request, data models.Templat "Notifications": data.Notifications, "MessageCount": data.MessageCount, "Messages": data.Messages, - "SiteName": cfg.Site.SiteName, - "CopyrightYearStart": cfg.Site.CopyrightYearStart, + "SiteName": meta.Name, + "CopyrightYearStart": meta.CopyrightYearStart, } } @@ -57,9 +66,8 @@ func TemplateFuncs() template.FuncMap { "min": func(a, b int) int { if a < b { return a - } else { - return b } + return b }, "intVal": func(p *int) int { if p == nil { @@ -102,12 +110,11 @@ func LoadTemplateFiles(name string, files ...string) *template.Template { "templates/main/footer.html", } all := append(shared, files...) - return template.Must(template.New(name).Funcs(TemplateFuncs()).ParseFiles(all...)) } func SetFlash(w http.ResponseWriter, r *http.Request, message string) { - session, _ := helpers.GetSession(w, r) + session, _ := httpHelpers.GetSession(w, r) session.Values["flash"] = message session.Save(r, w) } diff --git a/internal/helpers/template/error.go b/internal/helpers/template/error.go deleted file mode 100644 index 2e5ee7e..0000000 --- a/internal/helpers/template/error.go +++ /dev/null @@ -1,39 +0,0 @@ -package helpers - -import ( - "fmt" - "log" - "net/http" - "os" - - "synlotto-website/internal/models" -) - -func RenderError(w http.ResponseWriter, r *http.Request, statusCode int) { - log.Printf("βš™οΈ RenderError called with status: %d", statusCode) - - context := TemplateContext(w, r, models.TemplateData{}) - - pagePath := fmt.Sprintf("templates/error/%d.html", statusCode) - log.Printf("πŸ“„ Checking for template file: %s", pagePath) - - if _, err := os.Stat(pagePath); err != nil { - log.Printf("🚫 Template file missing: %s", err) - http.Error(w, http.StatusText(statusCode), statusCode) - return - } - - log.Println("βœ… Template file found, loading...") - - tmpl := LoadTemplateFiles(fmt.Sprintf("%d.html", statusCode), pagePath) - - w.WriteHeader(statusCode) - err := tmpl.ExecuteTemplate(w, "layout", context) - if err != nil { - log.Printf("❌ Failed to render error page layout: %v", err) - http.Error(w, http.StatusText(statusCode), statusCode) - return - } - - log.Println("βœ… Successfully rendered error page") // ToDo: log these to database -} diff --git a/internal/helpers/template/pagination.go b/internal/helpers/template/pagination.go index 6cf9919..972229b 100644 --- a/internal/helpers/template/pagination.go +++ b/internal/helpers/template/pagination.go @@ -4,6 +4,7 @@ import ( "database/sql" ) +// ToDo: Sql shouldnt be here. func GetTotalPages(db *sql.DB, tableName, whereClause string, args []interface{}, pageSize int) (totalPages, totalCount int) { query := "SELECT COUNT(*) FROM " + tableName + " " + whereClause row := db.QueryRow(query, args...) diff --git a/internal/http/middleware/auth.go b/internal/http/middleware/auth.go index 344b284..fb636c1 100644 --- a/internal/http/middleware/auth.go +++ b/internal/http/middleware/auth.go @@ -1,5 +1,6 @@ package middleware +// ToDo: will no doubt need to fix as now using new session not the olf gorilla one import ( "net/http" "time" diff --git a/internal/http/middleware/headers.go b/internal/http/middleware/headers.go index 0d2093d..568db2b 100644 --- a/internal/http/middleware/headers.go +++ b/internal/http/middleware/headers.go @@ -1,5 +1,6 @@ package middleware +// ToDo: make sure im using with gin import "net/http" func EnforceHTTPS(next http.Handler, enabled bool) http.Handler { diff --git a/internal/http/middleware/ratelimit.go b/internal/http/middleware/ratelimit.go index b658f16..6da688d 100644 --- a/internal/http/middleware/ratelimit.go +++ b/internal/http/middleware/ratelimit.go @@ -1,5 +1,6 @@ package middleware +// ToDo: make sure im using with gin import ( "net" "net/http" diff --git a/internal/http/middleware/recover.go b/internal/http/middleware/recover.go index da3d746..5fe6f79 100644 --- a/internal/http/middleware/recover.go +++ b/internal/http/middleware/recover.go @@ -1,5 +1,6 @@ package middleware +// ToDo: make sure im using with gin not to be confused with gins recovery but may do the same? import ( "log" "net/http" diff --git a/internal/http/middleware/scs.go b/internal/http/middleware/scs.go new file mode 100644 index 0000000..ebf56b2 --- /dev/null +++ b/internal/http/middleware/scs.go @@ -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) + } +} diff --git a/internal/http/middleware/sessiontimeout.go b/internal/http/middleware/sessiontimeout.go index 0fccdfa..476824f 100644 --- a/internal/http/middleware/sessiontimeout.go +++ b/internal/http/middleware/sessiontimeout.go @@ -1,5 +1,6 @@ package middleware +// ToDo: This is more than likele now redunant with the session change import ( "log" "net/http" diff --git a/internal/logging/config.go b/internal/logging/config.go index 5f1471f..9933b45 100644 --- a/internal/logging/config.go +++ b/internal/logging/config.go @@ -4,14 +4,11 @@ import ( "encoding/json" "log" - "synlotto-website/internal/models" + "synlotto-website/internal/platform/config" ) -func LogConfig(config *models.Config) { +func LogConfig(config *config.Config) { safeConfig := *config - safeConfig.CSRF.CSRFKey = "[REDACTED]" - safeConfig.Session.AuthKeyPath = "[REDACTED]" - safeConfig.Session.EncryptionKeyPath = "[REDACTED]" cfg, err := json.MarshalIndent(safeConfig, "", " ") if err != nil { diff --git a/internal/platform/bootstrap/csrf.go b/internal/platform/bootstrap/csrf.go deleted file mode 100644 index a576df6..0000000 --- a/internal/platform/bootstrap/csrf.go +++ /dev/null @@ -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 -} diff --git a/internal/platform/bootstrap/license.go b/internal/platform/bootstrap/license.go index cd4da4c..09ca59e 100644 --- a/internal/platform/bootstrap/license.go +++ b/internal/platform/bootstrap/license.go @@ -5,12 +5,12 @@ import ( "time" internal "synlotto-website/internal/licensecheck" - "synlotto-website/internal/models" + "synlotto-website/internal/platform/config" ) var globalChecker *internal.LicenseChecker -func InitLicenseChecker(config *models.Config) error { +func InitLicenseChecker(config *config.Config) error { checker := &internal.LicenseChecker{ LicenseAPIURL: config.License.APIURL, APIKey: config.License.APIKey, diff --git a/internal/platform/bootstrap/loader.go b/internal/platform/bootstrap/loader.go index c410f3d..4b86d43 100644 --- a/internal/platform/bootstrap/loader.go +++ b/internal/platform/bootstrap/loader.go @@ -5,11 +5,11 @@ import ( "fmt" "os" - "synlotto-website/internal/models" + "synlotto-website/internal/platform/config" ) type AppState struct { - Config *models.Config + Config *config.Config } func LoadAppState(configPath string) (*AppState, error) { @@ -19,7 +19,7 @@ func LoadAppState(configPath string) (*AppState, error) { } defer file.Close() - var config models.Config + var config config.Config if err := json.NewDecoder(file).Decode(&config); err != nil { return nil, fmt.Errorf("decode config: %w", err) } diff --git a/internal/platform/bootstrap/session.go b/internal/platform/bootstrap/session.go deleted file mode 100644 index 4192658..0000000 --- a/internal/platform/bootstrap/session.go +++ /dev/null @@ -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 -} diff --git a/internal/platform/config/config.go b/internal/platform/config/config.go index 3c10c70..eac0fd9 100644 --- a/internal/platform/config/config.go +++ b/internal/platform/config/config.go @@ -3,20 +3,20 @@ package config import ( "sync" - "synlotto-website/internal/models" + "synlotto-website/internal/platform/config" ) var ( - appConfig *models.Config + appConfig *config.Config once sync.Once ) -func Init(config *models.Config) { +func Init(config *config.Config) { once.Do(func() { appConfig = config }) } -func Get() *models.Config { +func Get() *config.Config { return appConfig } diff --git a/internal/platform/config/load.go b/internal/platform/config/load.go new file mode 100644 index 0000000..b9b2c57 --- /dev/null +++ b/internal/platform/config/load.go @@ -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 +} diff --git a/internal/platform/config/types.go b/internal/platform/config/types.go index e50f8ce..1b8dc0a 100644 --- a/internal/platform/config/types.go +++ b/internal/platform/config/types.go @@ -2,7 +2,7 @@ package config type Config struct { CSRF struct { - CSRFKey string `json:"csrfKey"` + CookieName string `json:"cookieName"` } `json:"csrf"` Database struct { @@ -28,9 +28,8 @@ type Config struct { } `json:"license"` Session struct { - AuthKeyPath string `json:"authKeyPath"` - EncryptionKeyPath string `json:"encryptionKeyPath"` - Name string `json:"name"` + Name string `json:"name"` + Lifetime string `json:"lifetime"` } `json:"session"` Site struct { diff --git a/internal/platform/csrf/csrf.go b/internal/platform/csrf/csrf.go new file mode 100644 index 0000000..becba36 --- /dev/null +++ b/internal/platform/csrf/csrf.go @@ -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 +} diff --git a/internal/platform/session/session.go b/internal/platform/session/session.go new file mode 100644 index 0000000..b246b41 --- /dev/null +++ b/internal/platform/session/session.go @@ -0,0 +1,25 @@ +package session + +import ( + "net/http" + "time" + + "synlotto-website/internal/platform/config" + + "github.com/alexedwards/scs/v2" +) + +func New(cfg config.Config) *scs.SessionManager { + lifetime := 12 * time.Hour + if d, err := time.ParseDuration(cfg.Session.Lifetime); err == nil && d > 0 { + lifetime = d + } + + s := scs.New() + s.Lifetime = lifetime + s.Cookie.Name = cfg.Session.Name + s.Cookie.HttpOnly = true + s.Cookie.SameSite = http.SameSiteLaxMode + s.Cookie.Secure = cfg.HttpServer.ProductionMode + return s +} diff --git a/internal/services/tickets/ticketmatching.go b/internal/services/tickets/ticketmatching.go index 4843f76..5ba01cb 100644 --- a/internal/services/tickets/ticketmatching.go +++ b/internal/services/tickets/ticketmatching.go @@ -8,11 +8,13 @@ import ( lotteryTicketHandlers "synlotto-website/internal/handlers/lottery/tickets" thunderballrules "synlotto-website/internal/rules/thunderball" services "synlotto-website/internal/services/draws" + lotteryTicketService "synlotto-website/internal/services/tickets" "synlotto-website/internal/helpers" "synlotto-website/internal/models" ) +// ToDo: SQL in here needs to me moved out the handler! func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, error) { stats := models.MatchRunStats{} @@ -212,7 +214,8 @@ func RefreshTicketPrizes(db *sql.DB) error { mainMatches := helpers.CountMatches(matchTicket.Balls, draw.Balls) bonusMatches := helpers.CountMatches(matchTicket.BonusBalls, draw.BonusBalls) - prizeTier := matcher.GetPrizeTier(row.GameType, mainMatches, bonusMatches, thunderballrules.ThunderballPrizeRules) + // ToDo: this isn't a lottery ticket service really its a draw one + prizeTier := lotteryTicketService.GetPrizeTier(row.GameType, mainMatches, bonusMatches, thunderballrules.ThunderballPrizeRules) isWinner := prizeTier != "" var label string diff --git a/internal/storage/mysql/auditLog/create.go b/internal/storage/auditlog/create.go similarity index 85% rename from internal/storage/mysql/auditLog/create.go rename to internal/storage/auditlog/create.go index c527232..7dd38d1 100644 --- a/internal/storage/mysql/auditLog/create.go +++ b/internal/storage/auditlog/create.go @@ -41,7 +41,8 @@ func AdminOnly(db *sql.DB, next http.HandlerFunc) http.HandlerFunc { }) } -func LogLoginAttempt(r *http.Request, username string, success bool) { +// 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? +func LogLoginAttempt(db *sql.DB, r *http.Request, username string, success bool) { ip := r.RemoteAddr userAgent := r.UserAgent() diff --git a/internal/storage/mysql/db.go b/internal/storage/db.go similarity index 100% rename from internal/storage/mysql/db.go rename to internal/storage/db.go diff --git a/internal/storage/mysql/messages/create.go b/internal/storage/messages/create.go similarity index 100% rename from internal/storage/mysql/messages/create.go rename to internal/storage/messages/create.go diff --git a/internal/storage/messages/delete.go b/internal/storage/messages/delete.go new file mode 100644 index 0000000..cbff833 --- /dev/null +++ b/internal/storage/messages/delete.go @@ -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 diff --git a/internal/storage/mysql/messages/read.go b/internal/storage/messages/read.go similarity index 100% rename from internal/storage/mysql/messages/read.go rename to internal/storage/messages/read.go diff --git a/internal/storage/mysql/messages/update.go b/internal/storage/messages/update.go similarity index 100% rename from internal/storage/mysql/messages/update.go rename to internal/storage/messages/update.go diff --git a/internal/storage/mysql/migrations/0001_initial_create.up.sql b/internal/storage/migrations/0001_initial_create.up.sql similarity index 100% rename from internal/storage/mysql/migrations/0001_initial_create.up.sql rename to internal/storage/migrations/0001_initial_create.up.sql diff --git a/internal/storage/mysql/messages/delete.go b/internal/storage/mysql/messages/delete.go deleted file mode 100644 index 27da811..0000000 --- a/internal/storage/mysql/messages/delete.go +++ /dev/null @@ -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 \ No newline at end of file diff --git a/internal/storage/mysql/sqlite-db-for-reference.go b/internal/storage/mysql/sqlite-db-for-reference.go deleted file mode 100644 index d7bfd33..0000000 --- a/internal/storage/mysql/sqlite-db-for-reference.go +++ /dev/null @@ -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 -} diff --git a/internal/storage/mysql/notifications/create.go b/internal/storage/notifications/create.go similarity index 100% rename from internal/storage/mysql/notifications/create.go rename to internal/storage/notifications/create.go diff --git a/internal/storage/mysql/notifications/delete.go b/internal/storage/notifications/delete.go similarity index 100% rename from internal/storage/mysql/notifications/delete.go rename to internal/storage/notifications/delete.go diff --git a/internal/storage/mysql/notifications/read.go b/internal/storage/notifications/read.go similarity index 100% rename from internal/storage/mysql/notifications/read.go rename to internal/storage/notifications/read.go diff --git a/internal/storage/mysql/notifications/update.go b/internal/storage/notifications/update.go similarity index 100% rename from internal/storage/mysql/notifications/update.go rename to internal/storage/notifications/update.go diff --git a/internal/storage/mysql/results/thunderball/create.go b/internal/storage/results/thunderball/create.go similarity index 100% rename from internal/storage/mysql/results/thunderball/create.go rename to internal/storage/results/thunderball/create.go diff --git a/internal/storage/mysql/schema.go b/internal/storage/schema.go similarity index 100% rename from internal/storage/mysql/schema.go rename to internal/storage/schema.go diff --git a/internal/storage/mysql/seeds/thunderball_seed.go b/internal/storage/seeds/thunderball_seed.go similarity index 100% rename from internal/storage/mysql/seeds/thunderball_seed.go rename to internal/storage/seeds/thunderball_seed.go diff --git a/internal/storage/mysql/statistics/thunderball/statisticqueries.go b/internal/storage/statistics/thunderball/statisticqueries.go similarity index 100% rename from internal/storage/mysql/statistics/thunderball/statisticqueries.go rename to internal/storage/statistics/thunderball/statisticqueries.go diff --git a/internal/storage/mysql/syndicate/create.go b/internal/storage/syndicate/create.go similarity index 100% rename from internal/storage/mysql/syndicate/create.go rename to internal/storage/syndicate/create.go diff --git a/internal/storage/mysql/syndicate/read.go b/internal/storage/syndicate/read.go similarity index 100% rename from internal/storage/mysql/syndicate/read.go rename to internal/storage/syndicate/read.go diff --git a/internal/storage/mysql/syndicate/syndicate.go b/internal/storage/syndicate/syndicate.go similarity index 100% rename from internal/storage/mysql/syndicate/syndicate.go rename to internal/storage/syndicate/syndicate.go diff --git a/internal/storage/mysql/syndicate/update.go b/internal/storage/syndicate/update.go similarity index 100% rename from internal/storage/mysql/syndicate/update.go rename to internal/storage/syndicate/update.go diff --git a/internal/storage/mysql/tickets/create.go b/internal/storage/tickets/create.go similarity index 100% rename from internal/storage/mysql/tickets/create.go rename to internal/storage/tickets/create.go diff --git a/internal/storage/mysql/users/create.go b/internal/storage/users/create.go similarity index 71% rename from internal/storage/mysql/users/create.go rename to internal/storage/users/create.go index a35c878..c83eed4 100644 --- a/internal/storage/mysql/users/create.go +++ b/internal/storage/users/create.go @@ -4,19 +4,19 @@ package storage import ( "context" "database/sql" - "errors" - - "synlotto-website/internal/models" ) -type UsersRepo struct{ db *sql.DB} +type UsersRepo struct{ db *sql.DB } -func NewUsersRepo(db *.sql.DB) *UsersRepo { return &UsersRepo{db: db} } +func NewUsersRepo(db *sql.DB) *UsersRepo { + return &UsersRepo{db: db} +} +// ToDo: should the function be in sql? 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 -} \ No newline at end of file +} diff --git a/internal/storage/mysql/users/read.go b/internal/storage/users/read.go similarity index 100% rename from internal/storage/mysql/users/read.go rename to internal/storage/users/read.go diff --git a/main.go b/main.go deleted file mode 100644 index c6367dc..0000000 --- a/main.go +++ /dev/null @@ -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") -}