diff --git a/bootstrap/session.go b/bootstrap/session.go index 326c2c6..336faea 100644 --- a/bootstrap/session.go +++ b/bootstrap/session.go @@ -4,11 +4,15 @@ import ( "bytes" "crypto/rand" "encoding/base64" + "encoding/gob" "fmt" "net/http" "os" + "time" + + sessionHandlers "synlotto-website/handlers/session" + sessionHelpers "synlotto-website/helpers/session" - helpers "synlotto-website/helpers/session" "synlotto-website/logging" "synlotto-website/models" @@ -17,12 +21,13 @@ import ( var ( sessionStore *sessions.CookieStore - sessionName string + Name string authKey []byte encryptKey []byte ) func InitSession(cfg *models.Config) error { + gob.Register(time.Time{}) authPath := cfg.Session.AuthKeyPath encPath := cfg.Session.EncryptionKeyPath @@ -32,7 +37,7 @@ func InitSession(cfg *models.Config) error { if err != nil { return err } - encoded := helpers.EncodeKey(key) + encoded := sessionHelpers.EncodeKey(key) err = os.WriteFile(authPath, []byte(encoded), 0600) if err != nil { return err @@ -45,7 +50,7 @@ func InitSession(cfg *models.Config) error { if err != nil { return err } - encoded := helpers.EncodeKey(key) + encoded := sessionHelpers.EncodeKey(key) err = os.WriteFile(encPath, []byte(encoded), 0600) if err != nil { return err @@ -96,15 +101,15 @@ func loadSessionKeys(authPath, encryptionPath, name string, isProduction bool) e return fmt.Errorf("auth and encryption keys must be 32 bytes each (got auth=%d, enc=%d)", len(authKey), len(encryptKey)) } - sessionStore = sessions.NewCookieStore(authKey, encryptKey) - sessionStore.Options = &sessions.Options{ + sessionHandlers.SessionStore = sessions.NewCookieStore(authKey, encryptKey) + sessionHandlers.SessionStore.Options = &sessions.Options{ Path: "/", - MaxAge: 86400 * 1, + MaxAge: 86400, HttpOnly: true, Secure: isProduction, SameSite: http.SameSiteLaxMode, } - sessionName = name + sessionHandlers.Name = name return nil } diff --git a/handlers/account/authentication.go b/handlers/account/authentication.go index 474335a..da209c8 100644 --- a/handlers/account/authentication.go +++ b/handlers/account/authentication.go @@ -5,8 +5,8 @@ import ( "net/http" "time" - securityHelpers "license-server/helpers/security" httpHelpers "synlotto-website/helpers/http" + securityHelpers "synlotto-website/helpers/security" templateHelpers "synlotto-website/helpers/template" "synlotto-website/models" "synlotto-website/storage" @@ -70,11 +70,11 @@ func Login(w http.ResponseWriter, r *http.Request) { } if user == nil || !securityHelpers.CheckPasswordHash(user.PasswordHash, password) { - storage.LogLoginAttempt(username, false) + storage.LogLoginAttempt(r, username, false) http.Error(w, "Invalid credentials", http.StatusUnauthorized) return } - storage.LogLoginAttempt(username, true) + storage.LogLoginAttempt(r, username, true) http.Redirect(w, r, "/", http.StatusSeeOther) } diff --git a/handlers/admin/dashboard.go b/handlers/admin/dashboard.go index 68f7149..d001289 100644 --- a/handlers/admin/dashboard.go +++ b/handlers/admin/dashboard.go @@ -5,13 +5,14 @@ import ( "log" "net/http" - helpers "synlotto-website/helpers" + httpHelpers "synlotto-website/helpers/http" templateHelpers "synlotto-website/helpers/template" + "synlotto-website/models" ) func AdminDashboardHandler(db *sql.DB) http.HandlerFunc { - return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { + return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { // userID, ok := securityHelpers.GetCurrentUserID(r) // if !ok { // http.Redirect(w, r, "/login", http.StatusSeeOther) diff --git a/handlers/admin/draws.go b/handlers/admin/draws.go index a5d11c7..adebaad 100644 --- a/handlers/admin/draws.go +++ b/handlers/admin/draws.go @@ -5,14 +5,14 @@ import ( "log" "net/http" - helpers "synlotto-website/helpers" + httpHelpers "synlotto-website/helpers/http" templateHelpers "synlotto-website/helpers/template" "synlotto-website/models" ) func NewDrawHandler(db *sql.DB) http.HandlerFunc { - return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { + return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { context := templateHelpers.TemplateContext(w, r, models.TemplateData{}) if r.Method == http.MethodPost { @@ -39,7 +39,7 @@ func NewDrawHandler(db *sql.DB) http.HandlerFunc { } func ModifyDrawHandler(db *sql.DB) http.HandlerFunc { - return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { + return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { id := r.FormValue("id") _, err := db.Exec(`UPDATE results_thunderball SET game_type=?, draw_date=?, ball_set=?, machine=? WHERE id=?`, @@ -58,7 +58,7 @@ func ModifyDrawHandler(db *sql.DB) http.HandlerFunc { } func DeleteDrawHandler(db *sql.DB) http.HandlerFunc { - return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { + return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { id := r.FormValue("id") _, err := db.Exec(`DELETE FROM results_thunderball WHERE id = ?`, id) @@ -73,7 +73,7 @@ func DeleteDrawHandler(db *sql.DB) http.HandlerFunc { } func ListDrawsHandler(db *sql.DB) http.HandlerFunc { - return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { + return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { context := templateHelpers.TemplateContext(w, r, models.TemplateData{}) draws := []models.DrawSummary{} diff --git a/handlers/admin/prizes.go b/handlers/admin/prizes.go index 6eec0be..9c35626 100644 --- a/handlers/admin/prizes.go +++ b/handlers/admin/prizes.go @@ -5,13 +5,15 @@ import ( "fmt" "net/http" "strconv" - helpers "synlotto-website/helpers" + + httpHelpers "synlotto-website/helpers/http" templateHelpers "synlotto-website/helpers/template" + "synlotto-website/models" ) func AddPrizesHandler(db *sql.DB) http.HandlerFunc { - return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { + return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { tmpl := templateHelpers.LoadTemplateFiles("add_prizes.html", "templates/admin/draws/prizes/add_prizes.html") @@ -43,7 +45,7 @@ func AddPrizesHandler(db *sql.DB) http.HandlerFunc { } func ModifyPrizesHandler(db *sql.DB) http.HandlerFunc { - return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { + return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { tmpl := templateHelpers.LoadTemplateFiles("modify_prizes.html", "templates/admin/draws/prizes/modify_prizes.html") diff --git a/handlers/lottery/draws/draw_handler.go b/handlers/lottery/draws/draw_handler.go index 6c5c9b3..6a914cb 100644 --- a/handlers/lottery/draws/draw_handler.go +++ b/handlers/lottery/draws/draw_handler.go @@ -9,6 +9,7 @@ import ( "synlotto-website/helpers" "synlotto-website/models" + "synlotto-website/storage" ) func NewDraw(db *sql.DB) http.HandlerFunc { @@ -19,7 +20,7 @@ func NewDraw(db *sql.DB) http.HandlerFunc { context["Page"] = "new_draw" context["Data"] = nil - tmpl := templateHelpers.LoadTemplateFiles("new_draw.html", "templates/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", "templates/admin/draws/new_draw.html") // ToDo: may need removing or moving add draw should be admin functionality and only when manually required. Potential live drawing of numbers in the future. err := tmpl.ExecuteTemplate(w, "layout", context) if err != nil { @@ -29,7 +30,7 @@ func NewDraw(db *sql.DB) http.HandlerFunc { } } -func Submit(w http.ResponseWriter, r *http.Request) { +func Submit(db *sql.DB, w http.ResponseWriter, r *http.Request) { log.Println("📝 Form submission received") _ = r.ParseForm() @@ -45,7 +46,12 @@ func Submit(w http.ResponseWriter, r *http.Request) { Thunderball: helpers.Atoi(r.FormValue("thunderball")), } - Draws = append(Draws, draw) + err := storage.InsertThunderballResult(db, draw) + if err != nil { + log.Println("❌ Failed to insert draw:", err) + http.Error(w, "Failed to save draw", http.StatusInternalServerError) + return + } log.Printf("📅 %s | 🛠 %s | 🎱 %d | 🔢 %d,%d,%d,%d,%d | ⚡ %d\n", draw.DrawDate, draw.Machine, draw.BallSet, diff --git a/handlers/lottery/tickets/ticket_handler.go b/handlers/lottery/tickets/ticket_handler.go index 54702eb..8d8869d 100644 --- a/handlers/lottery/tickets/ticket_handler.go +++ b/handlers/lottery/tickets/ticket_handler.go @@ -10,6 +10,7 @@ import ( "strconv" "time" + httpHelpers "synlotto-website/helpers/http" securityHelpers "synlotto-website/helpers/security" templateHelpers "synlotto-website/helpers/template" draws "synlotto-website/services/draws" @@ -21,7 +22,7 @@ import ( ) func AddTicket(db *sql.DB) http.HandlerFunc { - return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { + return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { rows, err := db.Query(` SELECT DISTINCT draw_date @@ -180,7 +181,7 @@ func AddTicket(db *sql.DB) http.HandlerFunc { } func SubmitTicket(db *sql.DB) http.HandlerFunc { - return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { + return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { err := r.ParseMultipartForm(10 << 20) if err != nil { http.Error(w, "Invalid form", http.StatusBadRequest) @@ -268,7 +269,7 @@ func SubmitTicket(db *sql.DB) http.HandlerFunc { } func GetMyTickets(db *sql.DB) http.HandlerFunc { - return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { + return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { http.Redirect(w, r, "/login", http.StatusSeeOther) diff --git a/handlers/messages.go b/handlers/messages.go index bd34cdc..416caf3 100644 --- a/handlers/messages.go +++ b/handlers/messages.go @@ -4,7 +4,6 @@ import ( "database/sql" "log" "net/http" - "strconv" templateHandlers "synlotto-website/handlers/template" "synlotto-website/helpers" @@ -23,7 +22,7 @@ func MessagesInboxHandler(db *sql.DB) http.HandlerFunc { return } - page := strconv.Atoi(r.URL.Query().Get("page")) + page := helpers.Atoi(r.URL.Query().Get("page")) if page < 1 { page = 1 } diff --git a/handlers/notifications.go b/handlers/notifications.go index 8b18985..008fce0 100644 --- a/handlers/notifications.go +++ b/handlers/notifications.go @@ -7,9 +7,9 @@ import ( "strconv" templateHandlers "synlotto-website/handlers/template" + httpHelpers "synlotto-website/helpers/http" templateHelpers "synlotto-website/helpers/template" - "synlotto-website/helpers" "synlotto-website/storage" ) @@ -37,7 +37,7 @@ func MarkNotificationReadHandler(db *sql.DB) http.HandlerFunc { return } - session, _ := helpers.GetSession(w, r) + session, _ := httpHelpers.GetSession(w, r) userID, ok := session.Values["user_id"].(int) if !ok { http.Error(w, "Unauthorized", http.StatusUnauthorized) diff --git a/handlers/results.go b/handlers/results.go index ff936fc..1352fe3 100644 --- a/handlers/results.go +++ b/handlers/results.go @@ -65,7 +65,7 @@ func ResultsThunderball(db *sql.DB) http.HandlerFunc { args = append(args, ballSetFilter) } - totalPages, totalResults := helpers.GetTotalPages(db, "results_thunderball", whereClause, args, pageSize) + totalPages, totalResults := templateHelpers.GetTotalPages(db, "results_thunderball", whereClause, args, pageSize) if page < 1 || page > totalPages { http.NotFound(w, r) return diff --git a/handlers/session/account.go b/handlers/session/account.go index a6743ad..06eb2be 100644 --- a/handlers/session/account.go +++ b/handlers/session/account.go @@ -1,16 +1,23 @@ package handlers import ( + "fmt" "net/http" "github.com/gorilla/sessions" ) var ( - sessionStore *sessions.CookieStore - sessionName string + SessionStore *sessions.CookieStore + Name string ) func GetSession(w http.ResponseWriter, r *http.Request) (*sessions.Session, error) { - return sessionStore.Get(r, sessionName) + 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/helpers/session.go b/helpers/session.go deleted file mode 100644 index d180514..0000000 --- a/helpers/session.go +++ /dev/null @@ -1,67 +0,0 @@ -package helpers - -import ( - "encoding/gob" - "net/http" - "time" - - "github.com/gorilla/sessions" -) - -var authKey = []byte("12345678901234567890123456789012") // ToDo: Make env var -var encryptKey = []byte("abcdefghijklmnopqrstuvwx12345678") // ToDo: Make env var -var sessionName = "synlotto-session" -var store = sessions.NewCookieStore(authKey, encryptKey) - -const SessionTimeout = 30 * time.Minute - -func init() { - gob.Register(time.Time{}) - - store.Options = &sessions.Options{ - Path: "/", - MaxAge: 86400 * 1, - HttpOnly: true, - Secure: false, // TODO: make env-configurable - SameSite: http.SameSiteLaxMode, - } -} - -func GetSession(w http.ResponseWriter, r *http.Request) (*sessions.Session, error) { - return store.Get(r, sessionName) -} - -func IsSessionExpired(session *sessions.Session) bool { - last, ok := session.Values["last_activity"].(time.Time) - if !ok { - return false - } - return time.Since(last) > SessionTimeout -} - -func UpdateSessionActivity(session *sessions.Session, r *http.Request, w http.ResponseWriter) { - session.Values["last_activity"] = time.Now() - 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, "/login", http.StatusSeeOther) - return - } - - UpdateSessionActivity(session, r, w) - - next(w, r) - } -} diff --git a/helpers/template/build.go b/helpers/template/build.go index 26fbdc8..946bc2b 100644 --- a/helpers/template/build.go +++ b/helpers/template/build.go @@ -29,16 +29,16 @@ func TemplateContext(w http.ResponseWriter, r *http.Request, data models.Templat } return map[string]interface{}{ - "CSRFField": csrf.TemplateField(r), - "Flash": flash, - "User": data.User, - "IsAdmin": data.IsAdmin, - "NotificationCount": data.NotificationCount, - "Notifications": data.Notifications, - "MessageCount": data.MessageCount, - "Messages": data.Messages, - "SiteName": cfg.Site.SiteName, - "YearStart": cfg.Site.CopyrightStart, + "CSRFField": csrf.TemplateField(r), + "Flash": flash, + "User": data.User, + "IsAdmin": data.IsAdmin, + "NotificationCount": data.NotificationCount, + "Notifications": data.Notifications, + "MessageCount": data.MessageCount, + "Messages": data.Messages, + "SiteName": cfg.Site.SiteName, + "CopyrightYearStart": cfg.Site.CopyrightYearStart, } } diff --git a/middleware/auth.go b/middleware/auth.go index fda7bd2..11d210a 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -4,14 +4,15 @@ import ( "net/http" "time" + httpHelpers "synlotto-website/helpers/http" + "synlotto-website/constants" - "synlotto-website/helpers" ) func Auth(required bool) func(http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - session, _ := helpers.GetSession(w, r) + session, _ := httpHelpers.GetSession(w, r) _, ok := session.Values["user_id"].(int) @@ -26,7 +27,7 @@ func Auth(required bool) func(http.HandlerFunc) http.HandlerFunc { session.Options.MaxAge = -1 session.Save(r, w) - newSession, _ := helpers.GetSession(w, r) + newSession, _ := httpHelpers.GetSession(w, r) newSession.Values["flash"] = "Your session has timed out." newSession.Save(r, w) diff --git a/models/config.go b/models/config.go index ee870ba..bb37e05 100644 --- a/models/config.go +++ b/models/config.go @@ -1,16 +1,16 @@ package models type Config struct { + CSRF struct { + CSRFKey string `json:"csrfKey"` + } `json:"csrf"` + HttpServer struct { Port int `json:"port"` Address string `json:"address"` ProductionMode bool `json:"productionMode"` } `json:"httpServer"` - CSRF struct { - CSRFKey string `json:"csrfKey"` - } `json:"csrf"` - License struct { APIURL string `json:"apiUrl"` APIKey string `json:"apiKey"` @@ -19,6 +19,11 @@ type Config struct { Session struct { AuthKeyPath string `json:"authKeyPath"` EncryptionKeyPath string `json:"encryptionKeyPath"` - Name string `json:"sessionName"` + Name string `json:"name"` } `json:"session"` + + Site struct { + SiteName string `json:"siteName"` + CopyrightYearStart int `json:"copyrightYearStart"` + } `json:"site"` } diff --git a/routes/accountroutes.go b/routes/accountroutes.go index feaf8fc..5ca96e2 100644 --- a/routes/accountroutes.go +++ b/routes/accountroutes.go @@ -5,6 +5,7 @@ import ( "net/http" account "synlotto-website/handlers/account" + lotteryDrawHandlers "synlotto-website/handlers/lottery/tickets" "synlotto-website/handlers" "synlotto-website/middleware" @@ -14,8 +15,8 @@ func SetupAccountRoutes(mux *http.ServeMux, db *sql.DB) { mux.HandleFunc("/login", middleware.Protected(account.Login)) mux.HandleFunc("/logout", account.Logout) mux.HandleFunc("/signup", middleware.Protected(account.Signup)) - mux.HandleFunc("/account/tickets/add_ticket", handlers.AddTicket(db)) - mux.HandleFunc("/account/tickets/my_tickets", handlers.GetMyTickets(db)) + mux.HandleFunc("/account/tickets/add_ticket", lotteryDrawHandlers.AddTicket(db)) + mux.HandleFunc("/account/tickets/my_tickets", lotteryDrawHandlers.GetMyTickets(db)) mux.HandleFunc("/account/messages", middleware.Protected(handlers.MessagesInboxHandler(db))) mux.HandleFunc("/account/messages/read", middleware.Protected(handlers.ReadMessageHandler(db))) mux.HandleFunc("/account/messages/archive", middleware.Protected(handlers.ArchiveMessageHandler(db))) diff --git a/storage/db.go b/storage/db.go index d99c27c..941a2f4 100644 --- a/storage/db.go +++ b/storage/db.go @@ -10,6 +10,8 @@ import ( _ "modernc.org/sqlite" ) +var db *sql.DB + func InitDB(filepath string) *sql.DB { var err error cfg := config.Get() diff --git a/templates/admin/draws/new_draw.html b/templates/admin/draws/new_draw.html index 456d6c5..a30a1e0 100644 --- a/templates/admin/draws/new_draw.html +++ b/templates/admin/draws/new_draw.html @@ -8,4 +8,35 @@ -{{ end }} \ No newline at end of file +{{ end }} + + + diff --git a/templates/new_draw.html b/templates/new_draw.html deleted file mode 100644 index 241eeb2..0000000 --- a/templates/new_draw.html +++ /dev/null @@ -1,27 +0,0 @@ -{{ define "content" }} -← Back -

Add New Thunderball Draw

-
- {{ .csrfField }} -
- -
-
- -
-
- -
-
- - - - - -
-
- -
- -
-{{ end }}