diff --git a/bootstrap/license.go b/bootstrap/license.go new file mode 100644 index 0000000..eb5c355 --- /dev/null +++ b/bootstrap/license.go @@ -0,0 +1,32 @@ +package bootstrap + +import ( + "log" + "time" + + internal "synlotto-website/internal/licensecheck" + "synlotto-website/models" +) + +var globalChecker *internal.LicenseChecker + +func InitLicenseChecker(config *models.Config) error { + checker := &internal.LicenseChecker{ + LicenseAPIURL: config.License.APIURL, + APIKey: config.License.APIKey, + PollInterval: 10 * time.Minute, + } + + if err := checker.Validate(); err != nil { + return err + } + + checker.StartBackgroundCheck() + globalChecker = checker + log.Println("✅ License validation started.") + return nil +} + +func GetLicenseChecker() *internal.LicenseChecker { + return globalChecker +} diff --git a/handlers/account.go b/handlers/account/authentication.go similarity index 74% rename from handlers/account.go rename to handlers/account/authentication.go index 8a092fb..474335a 100644 --- a/handlers/account.go +++ b/handlers/account/authentication.go @@ -3,23 +3,27 @@ package handlers import ( "log" "net/http" - "synlotto-website/helpers" - "synlotto-website/models" "time" + securityHelpers "license-server/helpers/security" + httpHelpers "synlotto-website/helpers/http" + templateHelpers "synlotto-website/helpers/template" + "synlotto-website/models" + "synlotto-website/storage" + "github.com/gorilla/csrf" ) func Login(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { - session, _ := helpers.GetSession(w, r) + session, _ := httpHelpers.GetSession(w, r) if _, ok := session.Values["user_id"].(int); ok { http.Redirect(w, r, "/", http.StatusSeeOther) return } - tmpl := helpers.LoadTemplateFiles("login.html", "templates/account/login.html") + tmpl := templateHelpers.LoadTemplateFiles("login.html", "templates/account/login.html") - context := helpers.TemplateContext(w, r, models.TemplateData{}) + context := templateHelpers.TemplateContext(w, r, models.TemplateData{}) context["csrfField"] = csrf.TemplateField(r) err := tmpl.ExecuteTemplate(w, "layout", context) @@ -34,12 +38,12 @@ func Login(w http.ResponseWriter, r *http.Request) { password := r.FormValue("password") user := models.GetUserByUsername(username) - if user == nil || !helpers.CheckPasswordHash(user.PasswordHash, password) { + if user == nil || !securityHelpers.CheckPasswordHash(user.PasswordHash, password) { http.Error(w, "Invalid credentials", http.StatusUnauthorized) return } - session, _ := helpers.GetSession(w, r) + session, _ := httpHelpers.GetSession(w, r) for k := range session.Values { delete(session.Values, k) @@ -65,18 +69,18 @@ func Login(w http.ResponseWriter, r *http.Request) { } } - if user == nil || !helpers.CheckPasswordHash(user.PasswordHash, password) { - models.LogLoginAttempt(username, false) + if user == nil || !securityHelpers.CheckPasswordHash(user.PasswordHash, password) { + storage.LogLoginAttempt(username, false) http.Error(w, "Invalid credentials", http.StatusUnauthorized) return } - models.LogLoginAttempt(username, true) + storage.LogLoginAttempt(username, true) http.Redirect(w, r, "/", http.StatusSeeOther) } func Logout(w http.ResponseWriter, r *http.Request) { - session, _ := helpers.GetSession(w, r) + session, _ := httpHelpers.GetSession(w, r) for k := range session.Values { delete(session.Values, k) @@ -95,7 +99,7 @@ func Logout(w http.ResponseWriter, r *http.Request) { func Signup(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { - tmpl := helpers.LoadTemplateFiles("signup.html", "templates/account/signup.html") + tmpl := templateHelpers.LoadTemplateFiles("signup.html", "templates/account/signup.html") tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ "csrfField": csrf.TemplateField(r), @@ -106,7 +110,7 @@ func Signup(w http.ResponseWriter, r *http.Request) { username := r.FormValue("username") password := r.FormValue("password") - hashed, err := helpers.HashPassword(password) + hashed, err := securityHelpers.HashPassword(password) if err != nil { http.Error(w, "Server error", http.StatusInternalServerError) return diff --git a/handlers/admin/audit.go b/handlers/admin/audit.go index 49e443f..2132e9c 100644 --- a/handlers/admin/audit.go +++ b/handlers/admin/audit.go @@ -4,7 +4,9 @@ import ( "database/sql" "log" "net/http" - "synlotto-website/helpers" + + templateHelpers "synlotto-website/helpers/template" + "synlotto-website/middleware" "synlotto-website/models" ) @@ -19,7 +21,7 @@ type AdminLogEntry struct { func AdminAccessLogHandler(db *sql.DB) http.HandlerFunc { return middleware.Auth(true)(func(w http.ResponseWriter, r *http.Request) { - context := helpers.TemplateContext(w, r, models.TemplateData{}) + context := templateHelpers.TemplateContext(w, r, models.TemplateData{}) rows, err := db.Query(` SELECT accessed_at, user_id, path, ip, user_agent @@ -45,7 +47,7 @@ func AdminAccessLogHandler(db *sql.DB) http.HandlerFunc { } context["AuditLogs"] = logs - tmpl := helpers.LoadTemplateFiles("access_log.html", "templates/admin/logs/access_log.html") + tmpl := templateHelpers.LoadTemplateFiles("access_log.html", "templates/admin/logs/access_log.html") _ = tmpl.ExecuteTemplate(w, "layout", context) }) @@ -53,7 +55,7 @@ func AdminAccessLogHandler(db *sql.DB) http.HandlerFunc { func AuditLogHandler(db *sql.DB) http.HandlerFunc { return middleware.Auth(true)(func(w http.ResponseWriter, r *http.Request) { - context := helpers.TemplateContext(w, r, models.TemplateData{}) + context := templateHelpers.TemplateContext(w, r, models.TemplateData{}) rows, err := db.Query(` SELECT timestamp, user_id, action, ip, user_agent @@ -81,7 +83,7 @@ func AuditLogHandler(db *sql.DB) http.HandlerFunc { context["AuditLogs"] = logs - tmpl := helpers.LoadTemplateFiles("audit.html", "templates/admin/logs/audit.html") + tmpl := templateHelpers.LoadTemplateFiles("audit.html", "templates/admin/logs/audit.html") err = tmpl.ExecuteTemplate(w, "layout", context) if err != nil { diff --git a/handlers/admin/dashboard.go b/handlers/admin/dashboard.go index 960881a..68f7149 100644 --- a/handlers/admin/dashboard.go +++ b/handlers/admin/dashboard.go @@ -6,12 +6,13 @@ import ( "net/http" helpers "synlotto-website/helpers" + 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) { - // userID, ok := helpers.GetCurrentUserID(r) + // userID, ok := securityHelpers.GetCurrentUserID(r) // if !ok { // http.Redirect(w, r, "/login", http.StatusSeeOther) // return @@ -19,7 +20,7 @@ func AdminDashboardHandler(db *sql.DB) http.HandlerFunc { // TODO: check is_admin from users table here - context := helpers.TemplateContext(w, r, models.TemplateData{}) + context := templateHelpers.TemplateContext(w, r, models.TemplateData{}) // Total ticket stats var total, winners int @@ -54,7 +55,7 @@ func AdminDashboardHandler(db *sql.DB) http.HandlerFunc { } context["MatchLogs"] = logs - tmpl := helpers.LoadTemplateFiles("dashboard.html", "templates/admin/dashboard.html") + tmpl := templateHelpers.LoadTemplateFiles("dashboard.html", "templates/admin/dashboard.html") err = tmpl.ExecuteTemplate(w, "layout", context) if err != nil { diff --git a/handlers/admin/draws.go b/handlers/admin/draws.go index ad569b2..a5d11c7 100644 --- a/handlers/admin/draws.go +++ b/handlers/admin/draws.go @@ -6,12 +6,14 @@ import ( "net/http" helpers "synlotto-website/helpers" + 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) { - context := helpers.TemplateContext(w, r, models.TemplateData{}) + context := templateHelpers.TemplateContext(w, r, models.TemplateData{}) if r.Method == http.MethodPost { game := r.FormValue("game_type") @@ -30,7 +32,7 @@ func NewDrawHandler(db *sql.DB) http.HandlerFunc { return } - tmpl := helpers.LoadTemplateFiles("new_draw", "templates/admin/draws/new_draw.html") + tmpl := templateHelpers.LoadTemplateFiles("new_draw", "templates/admin/draws/new_draw.html") tmpl.ExecuteTemplate(w, "layout", context) }) @@ -72,7 +74,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) { - context := helpers.TemplateContext(w, r, models.TemplateData{}) + context := templateHelpers.TemplateContext(w, r, models.TemplateData{}) draws := []models.DrawSummary{} rows, err := db.Query(` @@ -100,7 +102,7 @@ func ListDrawsHandler(db *sql.DB) http.HandlerFunc { context["Draws"] = draws - tmpl := helpers.LoadTemplateFiles("list.html", "templates/admin/draws/list.html") + tmpl := templateHelpers.LoadTemplateFiles("list.html", "templates/admin/draws/list.html") tmpl.ExecuteTemplate(w, "layout", context) }) diff --git a/handlers/admin/manualtriggers.go b/handlers/admin/manualtriggers.go index 6e9de20..59deef9 100644 --- a/handlers/admin/manualtriggers.go +++ b/handlers/admin/manualtriggers.go @@ -8,14 +8,15 @@ import ( "net/url" "strconv" - "synlotto-website/helpers" - "synlotto-website/models" + templateHelpers "synlotto-website/helpers/template" services "synlotto-website/services/tickets" + + "synlotto-website/models" ) func AdminTriggersHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - context := helpers.TemplateContext(w, r, models.TemplateData{}) + context := templateHelpers.TemplateContext(w, r, models.TemplateData{}) if flash := r.URL.Query().Get("flash"); flash != "" { context["Flash"] = flash @@ -71,7 +72,7 @@ func AdminTriggersHandler(db *sql.DB) http.HandlerFunc { return } - tmpl := helpers.LoadTemplateFiles("triggers.html", "templates/admin/triggers.html") + tmpl := templateHelpers.LoadTemplateFiles("triggers.html", "templates/admin/triggers.html") err := tmpl.ExecuteTemplate(w, "layout", context) if err != nil { diff --git a/handlers/admin/prizes.go b/handlers/admin/prizes.go index 59abe7f..6eec0be 100644 --- a/handlers/admin/prizes.go +++ b/handlers/admin/prizes.go @@ -5,16 +5,17 @@ import ( "fmt" "net/http" "strconv" - "synlotto-website/helpers" + helpers "synlotto-website/helpers" + 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) { if r.Method == http.MethodGet { - tmpl := helpers.LoadTemplateFiles("add_prizes.html", "templates/admin/draws/prizes/add_prizes.html") + tmpl := templateHelpers.LoadTemplateFiles("add_prizes.html", "templates/admin/draws/prizes/add_prizes.html") - tmpl.ExecuteTemplate(w, "layout", helpers.TemplateContext(w, r, models.TemplateData{})) + tmpl.ExecuteTemplate(w, "layout", templateHelpers.TemplateContext(w, r, models.TemplateData{})) return } @@ -44,9 +45,9 @@ func AddPrizesHandler(db *sql.DB) http.HandlerFunc { func ModifyPrizesHandler(db *sql.DB) http.HandlerFunc { return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { - tmpl := helpers.LoadTemplateFiles("modify_prizes.html", "templates/admin/draws/prizes/modify_prizes.html") + tmpl := templateHelpers.LoadTemplateFiles("modify_prizes.html", "templates/admin/draws/prizes/modify_prizes.html") - tmpl.ExecuteTemplate(w, "layout", helpers.TemplateContext(w, r, models.TemplateData{})) + tmpl.ExecuteTemplate(w, "layout", templateHelpers.TemplateContext(w, r, models.TemplateData{})) return } diff --git a/handlers/home.go b/handlers/home.go index 61effee..1e1d9d6 100644 --- a/handlers/home.go +++ b/handlers/home.go @@ -4,15 +4,17 @@ import ( "database/sql" "log" "net/http" - "synlotto-website/helpers" + + templateHandlers "synlotto-website/handlers/template" + templateHelpers "synlotto-website/helpers/template" ) func Home(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - data := BuildTemplateData(db, w, r) - context := helpers.TemplateContext(w, r, data) + data := templateHandlers.BuildTemplateData(db, w, r) + context := templateHelpers.TemplateContext(w, r, data) - tmpl := helpers.LoadTemplateFiles("index.html", "templates/index.html") + tmpl := templateHelpers.LoadTemplateFiles("index.html", "templates/index.html") err := tmpl.ExecuteTemplate(w, "layout", context) if err != nil { diff --git a/handlers/draw_handler.go b/handlers/lottery/draws/draw_handler.go similarity index 78% rename from handlers/draw_handler.go rename to handlers/lottery/draws/draw_handler.go index da04b6d..6c5c9b3 100644 --- a/handlers/draw_handler.go +++ b/handlers/lottery/draws/draw_handler.go @@ -5,6 +5,8 @@ import ( "log" "net/http" + templateHelpers "synlotto-website/helpers/template" + "synlotto-website/helpers" "synlotto-website/models" ) @@ -13,11 +15,11 @@ func NewDraw(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { log.Println("➡️ New draw form opened") - context := helpers.TemplateContext(w, r, models.TemplateData{}) + context := templateHelpers.TemplateContext(w, r, models.TemplateData{}) context["Page"] = "new_draw" context["Data"] = nil - tmpl := helpers.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/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 { diff --git a/handlers/syndicate.go b/handlers/lottery/syndicate/syndicate.go similarity index 60% rename from handlers/syndicate.go rename to handlers/lottery/syndicate/syndicate.go index 9f0a574..d3fa2ee 100644 --- a/handlers/syndicate.go +++ b/handlers/lottery/syndicate/syndicate.go @@ -5,6 +5,11 @@ import ( "fmt" "log" "net/http" + + templateHandlers "synlotto-website/handlers/template" + securityHelpers "synlotto-website/helpers/security" + templateHelpers "synlotto-website/helpers/template" + "synlotto-website/helpers" "synlotto-website/models" "synlotto-website/storage" @@ -14,18 +19,18 @@ func CreateSyndicateHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - data := BuildTemplateData(db, w, r) - context := helpers.TemplateContext(w, r, data) - tmpl := helpers.LoadTemplateFiles("create-syndicate.html", "templates/syndicate/create.html") + data := templateHandlers.BuildTemplateData(db, w, r) + context := templateHelpers.TemplateContext(w, r, data) + tmpl := templateHelpers.LoadTemplateFiles("create-syndicate.html", "templates/syndicate/create.html") tmpl.ExecuteTemplate(w, "layout", context) case http.MethodPost: name := r.FormValue("name") description := r.FormValue("description") - userId, ok := helpers.GetCurrentUserID(r) + userId, ok := securityHelpers.GetCurrentUserID(r) if !ok || name == "" { - helpers.SetFlash(w, r, "Invalid data submitted") + templateHelpers.SetFlash(w, r, "Invalid data submitted") http.Redirect(w, r, "/syndicate/create", http.StatusSeeOther) return } @@ -33,23 +38,23 @@ func CreateSyndicateHandler(db *sql.DB) http.HandlerFunc { _, err := storage.CreateSyndicate(db, userId, name, description) if err != nil { log.Printf("❌ CreateSyndicate failed: %v", err) - helpers.SetFlash(w, r, "Failed to create syndicate") + templateHelpers.SetFlash(w, r, "Failed to create syndicate") } else { - helpers.SetFlash(w, r, "Syndicate created successfully") + templateHelpers.SetFlash(w, r, "Syndicate created successfully") } http.Redirect(w, r, "/syndicate", http.StatusSeeOther) default: - helpers.RenderError(w, r, http.StatusMethodNotAllowed) + templateHelpers.RenderError(w, r, http.StatusMethodNotAllowed) } } } func ListSyndicatesHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - userID, ok := helpers.GetCurrentUserID(r) + userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { - helpers.RenderError(w, r, 403) // ToDo need to make this use the handler so i dont need to define errors. + templateHelpers.RenderError(w, r, 403) // ToDo need to make this use the handler so i dont need to define errors. return } @@ -68,28 +73,28 @@ func ListSyndicatesHandler(db *sql.DB) http.HandlerFunc { } } - data := BuildTemplateData(db, w, r) - context := helpers.TemplateContext(w, r, data) + data := templateHandlers.BuildTemplateData(db, w, r) + context := templateHelpers.TemplateContext(w, r, data) context["ManagedSyndicates"] = managed context["JoinedSyndicates"] = filteredJoined - tmpl := helpers.LoadTemplateFiles("syndicates.html", "templates/syndicate/index.html") + tmpl := templateHelpers.LoadTemplateFiles("syndicates.html", "templates/syndicate/index.html") tmpl.ExecuteTemplate(w, "layout", context) } } func ViewSyndicateHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - userID, ok := helpers.GetCurrentUserID(r) + userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { - helpers.RenderError(w, r, 403) + templateHelpers.RenderError(w, r, 403) return } syndicateID := helpers.Atoi(r.URL.Query().Get("id")) syndicate, err := storage.GetSyndicateByID(db, syndicateID) if err != nil || syndicate == nil { - helpers.RenderError(w, r, 404) + templateHelpers.RenderError(w, r, 404) return } @@ -97,45 +102,45 @@ func ViewSyndicateHandler(db *sql.DB) http.HandlerFunc { isMember := storage.IsSyndicateMember(db, syndicateID, userID) if !isManager && !isMember { - helpers.RenderError(w, r, 403) + templateHelpers.RenderError(w, r, 403) return } members := storage.GetSyndicateMembers(db, syndicateID) - data := BuildTemplateData(db, w, r) - context := helpers.TemplateContext(w, r, data) + data := templateHandlers.BuildTemplateData(db, w, r) + context := templateHelpers.TemplateContext(w, r, data) context["Syndicate"] = syndicate context["Members"] = members context["IsManager"] = isManager - tmpl := helpers.LoadTemplateFiles("syndicate-view.html", "templates/syndicate/view.html") + tmpl := templateHelpers.LoadTemplateFiles("syndicate-view.html", "templates/syndicate/view.html") tmpl.ExecuteTemplate(w, "layout", context) } } func SyndicateLogTicketHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - userID, ok := helpers.GetCurrentUserID(r) + userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { - helpers.RenderError(w, r, 403) + templateHelpers.RenderError(w, r, 403) return } syndicateId := helpers.Atoi(r.URL.Query().Get("id")) syndicate, err := storage.GetSyndicateByID(db, syndicateId) if err != nil || syndicate.OwnerID != userID { - helpers.RenderError(w, r, 403) + templateHelpers.RenderError(w, r, 403) return } switch r.Method { case http.MethodGet: - data := BuildTemplateData(db, w, r) - context := helpers.TemplateContext(w, r, data) + data := templateHandlers.BuildTemplateData(db, w, r) + context := templateHelpers.TemplateContext(w, r, data) context["Syndicate"] = syndicate - tmpl := helpers.LoadTemplateFiles("syndicate-log-ticket.html", "templates/syndicate/log_ticket.html") + tmpl := templateHelpers.LoadTemplateFiles("syndicate-log-ticket.html", "templates/syndicate/log_ticket.html") tmpl.ExecuteTemplate(w, "layout", context) case http.MethodPost: @@ -153,46 +158,46 @@ func SyndicateLogTicketHandler(db *sql.DB) http.HandlerFunc { }) if err != nil { - helpers.SetFlash(w, r, "Failed to add ticket.") + templateHelpers.SetFlash(w, r, "Failed to add ticket.") } else { - helpers.SetFlash(w, r, "Ticket added for syndicate.") + templateHelpers.SetFlash(w, r, "Ticket added for syndicate.") } http.Redirect(w, r, fmt.Sprintf("/syndicate/view?id=%d", syndicateId), http.StatusSeeOther) default: - helpers.RenderError(w, r, 405) + templateHelpers.RenderError(w, r, 405) } } } func SyndicateTicketsHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - userID, ok := helpers.GetCurrentUserID(r) + userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { - helpers.RenderError(w, r, 403) + templateHelpers.RenderError(w, r, 403) return } syndicateID := helpers.Atoi(r.URL.Query().Get("id")) if syndicateID == 0 { - helpers.RenderError(w, r, 400) + templateHelpers.RenderError(w, r, 400) return } if !storage.IsSyndicateMember(db, syndicateID, userID) { - helpers.RenderError(w, r, 403) + templateHelpers.RenderError(w, r, 403) return } tickets := storage.GetSyndicateTickets(db, syndicateID) - data := BuildTemplateData(db, w, r) - context := helpers.TemplateContext(w, r, data) + data := templateHandlers.BuildTemplateData(db, w, r) + context := templateHelpers.TemplateContext(w, r, data) context["SyndicateID"] = syndicateID context["Tickets"] = tickets - tmpl := helpers.LoadTemplateFiles("syndicate-tickets.html", "templates/syndicate/tickets.html") + tmpl := templateHelpers.LoadTemplateFiles("syndicate-tickets.html", "templates/syndicate/tickets.html") tmpl.ExecuteTemplate(w, "layout", context) } } diff --git a/handlers/syndicate_invites.go b/handlers/lottery/syndicate/syndicate_invites.go similarity index 66% rename from handlers/syndicate_invites.go rename to handlers/lottery/syndicate/syndicate_invites.go index 375150c..1abd6c0 100644 --- a/handlers/syndicate_invites.go +++ b/handlers/lottery/syndicate/syndicate_invites.go @@ -7,29 +7,33 @@ import ( "strconv" "time" + templateHandlers "synlotto-website/handlers/template" + securityHelpers "synlotto-website/helpers/security" + templateHelpers "synlotto-website/helpers/template" + "synlotto-website/helpers" "synlotto-website/storage" ) func SyndicateInviteHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - userID, ok := helpers.GetCurrentUserID(r) + userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { - helpers.RenderError(w, r, http.StatusForbidden) + templateHelpers.RenderError(w, r, http.StatusForbidden) return } switch r.Method { case http.MethodGet: syndicateID := helpers.Atoi(r.URL.Query().Get("id")) - data := BuildTemplateData(db, w, r) - context := helpers.TemplateContext(w, r, data) + data := templateHandlers.BuildTemplateData(db, w, r) + context := templateHelpers.TemplateContext(w, r, data) context["SyndicateID"] = syndicateID - tmpl := helpers.LoadTemplateFiles("invite-syndicate.html", "templates/syndicate/invite.html") + tmpl := templateHelpers.LoadTemplateFiles("invite-syndicate.html", "templates/syndicate/invite.html") err := tmpl.ExecuteTemplate(w, "layout", context) if err != nil { - helpers.RenderError(w, r, 500) + templateHelpers.RenderError(w, r, 500) } case http.MethodPost: syndicateID := helpers.Atoi(r.FormValue("syndicate_id")) @@ -37,32 +41,32 @@ func SyndicateInviteHandler(db *sql.DB) http.HandlerFunc { err := storage.InviteToSyndicate(db, userID, syndicateID, username) if err != nil { - helpers.SetFlash(w, r, "Failed to send invite: "+err.Error()) + templateHelpers.SetFlash(w, r, "Failed to send invite: "+err.Error()) } else { - helpers.SetFlash(w, r, "Invite sent successfully.") + templateHelpers.SetFlash(w, r, "Invite sent successfully.") } http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther) default: - helpers.RenderError(w, r, http.StatusMethodNotAllowed) + templateHelpers.RenderError(w, r, http.StatusMethodNotAllowed) } } } func ViewInvitesHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - userID, ok := helpers.GetCurrentUserID(r) + userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { - helpers.RenderError(w, r, 403) + templateHelpers.RenderError(w, r, 403) return } invites := storage.GetPendingInvites(db, userID) - data := BuildTemplateData(db, w, r) - context := helpers.TemplateContext(w, r, data) + data := templateHandlers.BuildTemplateData(db, w, r) + context := templateHelpers.TemplateContext(w, r, data) context["Invites"] = invites - tmpl := helpers.LoadTemplateFiles("invites.html", "templates/syndicate/invites.html") + tmpl := templateHelpers.LoadTemplateFiles("invites.html", "templates/syndicate/invites.html") tmpl.ExecuteTemplate(w, "layout", context) } } @@ -70,16 +74,16 @@ func ViewInvitesHandler(db *sql.DB) http.HandlerFunc { func AcceptInviteHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { inviteID := helpers.Atoi(r.URL.Query().Get("id")) - userID, ok := helpers.GetCurrentUserID(r) + userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { - helpers.RenderError(w, r, 403) + templateHelpers.RenderError(w, r, 403) return } err := storage.AcceptInvite(db, inviteID, userID) if err != nil { - helpers.SetFlash(w, r, "Failed to accept invite") + templateHelpers.SetFlash(w, r, "Failed to accept invite") } else { - helpers.SetFlash(w, r, "You have joined the syndicate") + templateHelpers.SetFlash(w, r, "You have joined the syndicate") } http.Redirect(w, r, "/syndicate", http.StatusSeeOther) } @@ -94,7 +98,7 @@ func DeclineInviteHandler(db *sql.DB) http.HandlerFunc { } func CreateInviteToken(db *sql.DB, syndicateID, invitedByID int, ttlHours int) (string, error) { - token, err := helpers.GenerateSecureToken() + token, err := securityHelpers.GenerateSecureToken() if err != nil { return "", err } @@ -142,16 +146,16 @@ func AcceptInviteToken(db *sql.DB, token string, userID int) error { func GenerateInviteLinkHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - userID, ok := helpers.GetCurrentUserID(r) + userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { - helpers.RenderError(w, r, http.StatusForbidden) + templateHelpers.RenderError(w, r, http.StatusForbidden) return } syndicateID := helpers.Atoi(r.URL.Query().Get("id")) - token, err := CreateInviteToken(db, syndicateID, userID, 48) // token valid for 48 hours + token, err := CreateInviteToken(db, syndicateID, userID, 48) if err != nil { - helpers.SetFlash(w, r, "Failed to generate invite link.") + templateHelpers.SetFlash(w, r, "Failed to generate invite link.") http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther) return } @@ -164,31 +168,31 @@ func GenerateInviteLinkHandler(db *sql.DB) http.HandlerFunc { } inviteLink := fmt.Sprintf("%s/syndicate/join?token=%s", origin, token) - helpers.SetFlash(w, r, "Invite link created: "+inviteLink) + templateHelpers.SetFlash(w, r, "Invite link created: "+inviteLink) http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther) } } func JoinSyndicateWithTokenHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - userID, ok := helpers.GetCurrentUserID(r) + userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { - helpers.RenderError(w, r, http.StatusForbidden) + templateHelpers.RenderError(w, r, http.StatusForbidden) return } token := r.URL.Query().Get("token") if token == "" { - helpers.SetFlash(w, r, "Invalid or missing invite token.") + templateHelpers.SetFlash(w, r, "Invalid or missing invite token.") http.Redirect(w, r, "/syndicate", http.StatusSeeOther) return } err := AcceptInviteToken(db, token, userID) if err != nil { - helpers.SetFlash(w, r, "Failed to join syndicate: "+err.Error()) + templateHelpers.SetFlash(w, r, "Failed to join syndicate: "+err.Error()) } else { - helpers.SetFlash(w, r, "You have joined the syndicate!") + templateHelpers.SetFlash(w, r, "You have joined the syndicate!") } http.Redirect(w, r, "/syndicate", http.StatusSeeOther) } @@ -196,27 +200,27 @@ func JoinSyndicateWithTokenHandler(db *sql.DB) http.HandlerFunc { func ManageInviteTokensHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - userID, ok := helpers.GetCurrentUserID(r) + userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { - helpers.RenderError(w, r, 403) + templateHelpers.RenderError(w, r, 403) return } syndicateID := helpers.Atoi(r.URL.Query().Get("id")) if !storage.IsSyndicateManager(db, syndicateID, userID) { - helpers.RenderError(w, r, 403) + templateHelpers.RenderError(w, r, 403) return } tokens := storage.GetInviteTokensForSyndicate(db, syndicateID) - data := BuildTemplateData(db, w, r) - context := helpers.TemplateContext(w, r, data) + data := templateHandlers.BuildTemplateData(db, w, r) + context := templateHelpers.TemplateContext(w, r, data) context["Tokens"] = tokens context["SyndicateID"] = syndicateID - tmpl := helpers.LoadTemplateFiles("invite-links.html", "templates/syndicate/invite_links.html") + tmpl := templateHelpers.LoadTemplateFiles("invite-links.html", "templates/syndicate/invite_links.html") tmpl.ExecuteTemplate(w, "layout", context) } } diff --git a/handlers/ticket_handler.go b/handlers/lottery/tickets/ticket_handler.go similarity index 93% rename from handlers/ticket_handler.go rename to handlers/lottery/tickets/ticket_handler.go index 4ad0268..54702eb 100644 --- a/handlers/ticket_handler.go +++ b/handlers/lottery/tickets/ticket_handler.go @@ -8,10 +8,14 @@ import ( "net/http" "os" "strconv" + "time" + + securityHelpers "synlotto-website/helpers/security" + templateHelpers "synlotto-website/helpers/template" + draws "synlotto-website/services/draws" + "synlotto-website/helpers" "synlotto-website/models" - draws "synlotto-website/services/draws" - "time" "github.com/gorilla/csrf" ) @@ -39,11 +43,11 @@ func AddTicket(db *sql.DB) http.HandlerFunc { } } - context := helpers.TemplateContext(w, r, models.TemplateData{}) + context := templateHelpers.TemplateContext(w, r, models.TemplateData{}) context["csrfField"] = csrf.TemplateField(r) context["DrawDates"] = drawDates - tmpl := helpers.LoadTemplateFiles("add_ticket.html", "templates/account/tickets/add_ticket.html") + tmpl := templateHelpers.LoadTemplateFiles("add_ticket.html", "templates/account/tickets/add_ticket.html") err = tmpl.ExecuteTemplate(w, "layout", context) if err != nil { @@ -60,7 +64,7 @@ func AddTicket(db *sql.DB) http.HandlerFunc { return } - userID, ok := helpers.GetCurrentUserID(r) + userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { http.Redirect(w, r, "/login", http.StatusSeeOther) return @@ -183,7 +187,7 @@ func SubmitTicket(db *sql.DB) http.HandlerFunc { return } - userID, ok := helpers.GetCurrentUserID(r) + userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { http.Redirect(w, r, "/login", http.StatusSeeOther) return @@ -265,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) { - userID, ok := helpers.GetCurrentUserID(r) + userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { http.Redirect(w, r, "/login", http.StatusSeeOther) return @@ -355,10 +359,10 @@ func GetMyTickets(db *sql.DB) http.HandlerFunc { tickets = append(tickets, t) } - context := helpers.TemplateContext(w, r, models.TemplateData{}) + context := templateHelpers.TemplateContext(w, r, models.TemplateData{}) context["Tickets"] = tickets - tmpl := helpers.LoadTemplateFiles("my_tickets.html", "templates/account/tickets/my_tickets.html") + tmpl := templateHelpers.LoadTemplateFiles("my_tickets.html", "templates/account/tickets/my_tickets.html") err = tmpl.ExecuteTemplate(w, "layout", context) if err != nil { diff --git a/handlers/ticket_matcher.go b/handlers/lottery/tickets/ticket_matcher.go similarity index 100% rename from handlers/ticket_matcher.go rename to handlers/lottery/tickets/ticket_matcher.go diff --git a/handlers/messages.go b/handlers/messages.go index 47a167f..bd34cdc 100644 --- a/handlers/messages.go +++ b/handlers/messages.go @@ -4,20 +4,26 @@ import ( "database/sql" "log" "net/http" + "strconv" + templateHandlers "synlotto-website/handlers/template" "synlotto-website/helpers" + httpHelpers "synlotto-website/helpers/http" + securityHelpers "synlotto-website/helpers/security" + templateHelpers "synlotto-website/helpers/template" + "synlotto-website/storage" ) func MessagesInboxHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - userID, ok := helpers.GetCurrentUserID(r) + userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { - helpers.RenderError(w, r, 403) + templateHelpers.RenderError(w, r, 403) return } - page := helpers.Atoi(r.URL.Query().Get("page")) + page := strconv.Atoi(r.URL.Query().Get("page")) if page < 1 { page = 1 } @@ -31,18 +37,18 @@ func MessagesInboxHandler(db *sql.DB) http.HandlerFunc { messages := storage.GetInboxMessages(db, userID, page, perPage) - data := BuildTemplateData(db, w, r) - context := helpers.TemplateContext(w, r, data) + data := templateHandlers.BuildTemplateData(db, w, r) + context := templateHelpers.TemplateContext(w, r, data) context["Messages"] = messages context["CurrentPage"] = page context["TotalPages"] = totalPages - context["PageRange"] = helpers.PageRange(page, totalPages) + context["PageRange"] = templateHelpers.PageRange(page, totalPages) - tmpl := helpers.LoadTemplateFiles("messages.html", "templates/account/messages/index.html") + tmpl := templateHelpers.LoadTemplateFiles("messages.html", "templates/account/messages/index.html") if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil { - helpers.RenderError(w, r, 500) + templateHelpers.RenderError(w, r, 500) } } } @@ -52,10 +58,10 @@ func ReadMessageHandler(db *sql.DB) http.HandlerFunc { idStr := r.URL.Query().Get("id") messageID := helpers.Atoi(idStr) - session, _ := helpers.GetSession(w, r) + session, _ := httpHelpers.GetSession(w, r) userID, ok := session.Values["user_id"].(int) if !ok { - helpers.RenderError(w, r, 403) + templateHelpers.RenderError(w, r, 403) return } @@ -67,11 +73,11 @@ func ReadMessageHandler(db *sql.DB) http.HandlerFunc { _ = storage.MarkMessageAsRead(db, messageID, userID) } - data := BuildTemplateData(db, w, r) - context := helpers.TemplateContext(w, r, data) + data := templateHandlers.BuildTemplateData(db, w, r) + context := templateHelpers.TemplateContext(w, r, data) context["Message"] = message - tmpl := helpers.LoadTemplateFiles("read-message.html", "templates/account/messages/read.html") + tmpl := templateHelpers.LoadTemplateFiles("read-message.html", "templates/account/messages/read.html") tmpl.ExecuteTemplate(w, "layout", context) } @@ -80,17 +86,17 @@ func ReadMessageHandler(db *sql.DB) http.HandlerFunc { func ArchiveMessageHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := helpers.Atoi(r.URL.Query().Get("id")) - userID, ok := helpers.GetCurrentUserID(r) + userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { - helpers.RenderError(w, r, 403) + templateHelpers.RenderError(w, r, 403) return } err := storage.ArchiveMessage(db, userID, id) if err != nil { - helpers.SetFlash(w, r, "Failed to archive message.") + templateHelpers.SetFlash(w, r, "Failed to archive message.") } else { - helpers.SetFlash(w, r, "Message archived.") + templateHelpers.SetFlash(w, r, "Message archived.") } http.Redirect(w, r, "/account/messages", http.StatusSeeOther) @@ -99,9 +105,9 @@ func ArchiveMessageHandler(db *sql.DB) http.HandlerFunc { func ArchivedMessagesHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - userID, ok := helpers.GetCurrentUserID(r) + userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { - helpers.RenderError(w, r, 403) + templateHelpers.RenderError(w, r, 403) return } @@ -114,13 +120,13 @@ func ArchivedMessagesHandler(db *sql.DB) http.HandlerFunc { messages := storage.GetArchivedMessages(db, userID, page, perPage) hasMore := len(messages) == perPage - data := BuildTemplateData(db, w, r) - context := helpers.TemplateContext(w, r, data) + data := templateHandlers.BuildTemplateData(db, w, r) + context := templateHelpers.TemplateContext(w, r, data) context["Messages"] = messages context["Page"] = page context["HasMore"] = hasMore - tmpl := helpers.LoadTemplateFiles("archived.html", "templates/account/messages/archived.html") + tmpl := templateHelpers.LoadTemplateFiles("archived.html", "templates/account/messages/archived.html") tmpl.ExecuteTemplate(w, "layout", context) } } @@ -129,19 +135,17 @@ func SendMessageHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - // Display the form - data := BuildTemplateData(db, w, r) - context := helpers.TemplateContext(w, r, data) - tmpl := helpers.LoadTemplateFiles("send-message.html", "templates/account/messages/send.html") + data := templateHandlers.BuildTemplateData(db, w, r) + context := templateHelpers.TemplateContext(w, r, data) + tmpl := templateHelpers.LoadTemplateFiles("send-message.html", "templates/account/messages/send.html") if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil { - helpers.RenderError(w, r, 500) + templateHelpers.RenderError(w, r, 500) } case http.MethodPost: - // Handle form submission - senderID, ok := helpers.GetCurrentUserID(r) + senderID, ok := securityHelpers.GetCurrentUserID(r) if !ok { - helpers.RenderError(w, r, 403) + templateHelpers.RenderError(w, r, 403) return } @@ -150,13 +154,13 @@ func SendMessageHandler(db *sql.DB) http.HandlerFunc { body := r.FormValue("message") if err := storage.SendMessage(db, senderID, recipientID, subject, body); err != nil { - helpers.SetFlash(w, r, "Failed to send message.") + templateHelpers.SetFlash(w, r, "Failed to send message.") } else { - helpers.SetFlash(w, r, "Message sent.") + templateHelpers.SetFlash(w, r, "Message sent.") } http.Redirect(w, r, "/account/messages", http.StatusSeeOther) default: - helpers.RenderError(w, r, 405) + templateHelpers.RenderError(w, r, 405) } } } @@ -164,17 +168,17 @@ func SendMessageHandler(db *sql.DB) http.HandlerFunc { func RestoreMessageHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := helpers.Atoi(r.URL.Query().Get("id")) - userID, ok := helpers.GetCurrentUserID(r) + userID, ok := securityHelpers.GetCurrentUserID(r) if !ok { - helpers.RenderError(w, r, 403) + templateHelpers.RenderError(w, r, 403) return } err := storage.RestoreMessage(db, userID, id) if err != nil { - helpers.SetFlash(w, r, "Failed to restore message.") + templateHelpers.SetFlash(w, r, "Failed to restore message.") } else { - helpers.SetFlash(w, r, "Message restored.") + templateHelpers.SetFlash(w, r, "Message restored.") } http.Redirect(w, r, "/account/messages/archived", http.StatusSeeOther) diff --git a/handlers/notifications.go b/handlers/notifications.go index d0c9fed..8b18985 100644 --- a/handlers/notifications.go +++ b/handlers/notifications.go @@ -6,16 +6,19 @@ import ( "net/http" "strconv" + templateHandlers "synlotto-website/handlers/template" + templateHelpers "synlotto-website/helpers/template" + "synlotto-website/helpers" "synlotto-website/storage" ) func NotificationsHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - data := BuildTemplateData(db, w, r) - context := helpers.TemplateContext(w, r, data) + data := templateHandlers.BuildTemplateData(db, w, r) + context := templateHelpers.TemplateContext(w, r, data) - tmpl := helpers.LoadTemplateFiles("index.html", "templates/account/notifications/index.html") + tmpl := templateHelpers.LoadTemplateFiles("index.html", "templates/account/notifications/index.html") err := tmpl.ExecuteTemplate(w, "layout", context) if err != nil { @@ -52,11 +55,11 @@ func MarkNotificationReadHandler(db *sql.DB) http.HandlerFunc { } } - data := BuildTemplateData(db, w, r) - context := helpers.TemplateContext(w, r, data) + data := templateHandlers.BuildTemplateData(db, w, r) + context := templateHelpers.TemplateContext(w, r, data) context["Notification"] = notification - tmpl := helpers.LoadTemplateFiles("read.html", "templates/account/notifications/read.html") + tmpl := templateHelpers.LoadTemplateFiles("read.html", "templates/account/notifications/read.html") err = tmpl.ExecuteTemplate(w, "layout", context) if err != nil { diff --git a/handlers/results.go b/handlers/results.go index fa18262..ff936fc 100644 --- a/handlers/results.go +++ b/handlers/results.go @@ -9,6 +9,8 @@ import ( "sort" "strconv" + templateHelpers "synlotto-website/helpers/template" + "synlotto-website/helpers" "synlotto-website/middleware" "synlotto-website/models" @@ -111,7 +113,7 @@ func ResultsThunderball(db *sql.DB) http.HandlerFunc { noResultsMsg = "No results found for \"" + query + "\"" } - tmpl := helpers.LoadTemplateFiles("thunderball.html", "templates/results/thunderball.html") + tmpl := templateHelpers.LoadTemplateFiles("thunderball.html", "templates/results/thunderball.html") err = tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ "Results": results, diff --git a/handlers/template_context.go b/handlers/template/templatedata.go similarity index 52% rename from handlers/template_context.go rename to handlers/template/templatedata.go index 4c55913..8875a6a 100644 --- a/handlers/template_context.go +++ b/handlers/template/templatedata.go @@ -2,14 +2,20 @@ package handlers import ( "database/sql" + "log" "net/http" - "synlotto-website/helpers" + + httpHelper "synlotto-website/helpers/http" + "synlotto-website/models" "synlotto-website/storage" ) func BuildTemplateData(db *sql.DB, w http.ResponseWriter, r *http.Request) models.TemplateData { - session, _ := helpers.GetSession(w, r) + session, err := httpHelper.GetSession(w, r) + if err != nil { + log.Printf("Session error: %v", err) + } var user *models.User var isAdmin bool @@ -18,19 +24,15 @@ func BuildTemplateData(db *sql.DB, w http.ResponseWriter, r *http.Request) model var messageCount int var messages []models.Message - switch v := session.Values["user_id"].(type) { - case int: - user = models.GetUserByID(v) // ToDo should be storage not models - case int64: - user = models.GetUserByID(int(v)) - } - - 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) + if userId, ok := session.Values["user_id"].(int); ok { + user = storage.GetUserByID(db, userId) + if user != nil { + isAdmin = user.IsAdmin + notificationCount = storage.GetNotificationCount(db, user.Id) + notifications = storage.GetRecentNotifications(db, user.Id, 15) + messageCount, _ = storage.GetMessageCount(db, user.Id) + messages = storage.GetRecentMessages(db, user.Id, 15) + } } return models.TemplateData{ diff --git a/helpers/http/session.go b/helpers/http/session.go new file mode 100644 index 0000000..c43c07b --- /dev/null +++ b/helpers/http/session.go @@ -0,0 +1,51 @@ +package helpers + +import ( + "net/http" + "time" + + session "synlotto-website/handlers/session" + + "synlotto-website/constants" + + "github.com/gorilla/sessions" +) + +func GetSession(w http.ResponseWriter, r *http.Request) (*sessions.Session, error) { + return session.GetSession(w, r) +} + +func IsSessionExpired(session *sessions.Session) bool { + last, ok := session.Values["last_activity"].(time.Time) + if !ok { + return false + } + return time.Since(last) > constants.SessionDuration +} + +func UpdateSessionActivity(session *sessions.Session, r *http.Request, w http.ResponseWriter) { + session.Values["last_activity"] = time.Now().UTC() + session.Save(r, w) +} + +func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session, _ := GetSession(w, r) + + if IsSessionExpired(session) { + session.Options.MaxAge = -1 + session.Save(r, w) + + newSession, _ := GetSession(w, r) + newSession.Values["flash"] = "Your session has timed out." + newSession.Save(r, w) + + http.Redirect(w, r, "/account/login", http.StatusSeeOther) + return + } + + UpdateSessionActivity(session, r, w) + + next(w, r) + } +} diff --git a/helpers/database.go b/helpers/security/admin.go similarity index 94% rename from helpers/database.go rename to helpers/security/admin.go index 9b66e56..047c870 100644 --- a/helpers/database.go +++ b/helpers/security/admin.go @@ -1,4 +1,4 @@ -package helpers +package security import ( "database/sql" diff --git a/helpers/auth.go b/helpers/security/password.go similarity index 95% rename from helpers/auth.go rename to helpers/security/password.go index 8bc848f..34fc92f 100644 --- a/helpers/auth.go +++ b/helpers/security/password.go @@ -1,4 +1,4 @@ -package helpers +package security import "golang.org/x/crypto/bcrypt" diff --git a/helpers/token.go b/helpers/security/token.go similarity index 92% rename from helpers/token.go rename to helpers/security/token.go index 5391a84..d63b048 100644 --- a/helpers/token.go +++ b/helpers/security/token.go @@ -1,4 +1,4 @@ -package helpers +package security import ( "crypto/rand" diff --git a/helpers/security/users.go b/helpers/security/users.go new file mode 100644 index 0000000..f31c428 --- /dev/null +++ b/helpers/security/users.go @@ -0,0 +1,17 @@ +package security + +import ( + "net/http" + + httpHelpers "synlotto-website/helpers/http" +) + +func GetCurrentUserID(r *http.Request) (int, bool) { + session, err := httpHelpers.GetSession(nil, r) + if err != nil { + return 0, false + } + + id, ok := session.Values["user_id"].(int) + return id, ok +} diff --git a/helpers/session.go b/helpers/session.go index 3429c55..d180514 100644 --- a/helpers/session.go +++ b/helpers/session.go @@ -65,13 +65,3 @@ func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc { next(w, r) } } - -func GetCurrentUserID(r *http.Request) (int, bool) { - session, err := GetSession(nil, r) - if err != nil { - return 0, false - } - - id, ok := session.Values["user_id"].(int) - return id, ok -} diff --git a/helpers/template.go b/helpers/template/build.go similarity index 72% rename from helpers/template.go rename to helpers/template/build.go index d7d165d..26fbdc8 100644 --- a/helpers/template.go +++ b/helpers/template/build.go @@ -5,14 +5,21 @@ import ( "log" "net/http" "strings" + "time" + "synlotto-website/config" + helpers "synlotto-website/helpers/http" "synlotto-website/models" "github.com/gorilla/csrf" ) func TemplateContext(w http.ResponseWriter, r *http.Request, data models.TemplateData) map[string]interface{} { - session, _ := GetSession(w, r) + cfg := config.Get() + if cfg == nil { + log.Println("⚠️ Config not initialized!") + } + session, _ := helpers.GetSession(w, r) var flash string if f, ok := session.Values["flash"].(string); ok { @@ -30,6 +37,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, + "YearStart": cfg.Site.CopyrightStart, } } @@ -58,9 +67,8 @@ func TemplateFuncs() template.FuncMap { } return *p }, - "inSlice": InSlice, - "lower": lower, - "rangeClass": rangeClass, + "inSlice": InSlice, + "lower": lower, "truncate": func(s string, max int) string { if len(s) <= max { return s @@ -68,22 +76,37 @@ func TemplateFuncs() template.FuncMap { return s[:max] + "..." }, "PageRange": PageRange, + "now": time.Now, + "humanTime": func(v interface{}) string { + switch t := v.(type) { + case time.Time: + return t.Local().Format("02 Jan 2006 15:04") + case string: + parsed, err := time.Parse(time.RFC3339, t) + if err == nil { + return parsed.Local().Format("02 Jan 2006 15:04") + } + return t + default: + return "" + } + }, } } func LoadTemplateFiles(name string, files ...string) *template.Template { shared := []string{ - "templates/layout.html", - "templates/topbar.html", + "templates/main/layout.html", + "templates/main/topbar.html", + "templates/main/footer.html", } all := append(shared, files...) - log.Printf("📄 Loading templates: %v", all) return template.Must(template.New(name).Funcs(TemplateFuncs()).ParseFiles(all...)) } func SetFlash(w http.ResponseWriter, r *http.Request, message string) { - session, _ := GetSession(w, r) + session, _ := helpers.GetSession(w, r) session.Values["flash"] = message session.Save(r, w) } @@ -101,6 +124,15 @@ func lower(input string) string { return strings.ToLower(input) } +func PageRange(current, total int) []int { + var pages []int + for i := 1; i <= total; i++ { + pages = append(pages, i) + } + return pages +} + +// ToDo: Should be ball range class, and should it even be here? func rangeClass(n int) string { switch { case n >= 1 && n <= 9: @@ -117,11 +149,3 @@ func rangeClass(n int) string { return "50-plus" } } - -func PageRange(current, total int) []int { - var pages []int - for i := 1; i <= total; i++ { - pages = append(pages, i) - } - return pages -} diff --git a/helpers/pages.go b/helpers/template/error.go similarity index 95% rename from helpers/pages.go rename to helpers/template/error.go index 0c6e3af..7a3b3bf 100644 --- a/helpers/pages.go +++ b/helpers/template/error.go @@ -5,6 +5,7 @@ import ( "log" "net/http" "os" + "synlotto-website/models" ) @@ -36,5 +37,3 @@ func RenderError(w http.ResponseWriter, r *http.Request, statusCode int) { log.Println("✅ Successfully rendered error page") // ToDo: log these to database } - -//ToDo Pages.go /template.go to be merged? diff --git a/helpers/pagination.go b/helpers/template/pagination.go similarity index 100% rename from helpers/pagination.go rename to helpers/template/pagination.go diff --git a/internal/licensecheck/checker.go b/internal/licensecheck/checker.go new file mode 100644 index 0000000..80c6c3f --- /dev/null +++ b/internal/licensecheck/checker.go @@ -0,0 +1,25 @@ +package internal + +import ( + "sync" + "time" +) + +type LicenseChecker struct { + LicenseAPIURL string + APIKey string + PollInterval time.Duration + + mu sync.RWMutex + lastGood time.Time + valid bool +} + +func (lc *LicenseChecker) setValid(ok bool) { + lc.mu.Lock() + defer lc.mu.Unlock() + lc.valid = ok + if ok { + lc.lastGood = time.Now() + } +} diff --git a/internal/licensecheck/validate.go b/internal/licensecheck/validate.go new file mode 100644 index 0000000..2a2cb57 --- /dev/null +++ b/internal/licensecheck/validate.go @@ -0,0 +1,76 @@ +package internal + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "time" +) + +func (lc *LicenseChecker) Validate() error { + url := fmt.Sprintf("%s/license/lookup?key=%s&format=json", lc.LicenseAPIURL, lc.APIKey) + resp, err := http.Get(url) + if err != nil { + lc.setValid(false) + + return fmt.Errorf("license lookup failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + lc.setValid(false) + + return fmt.Errorf("license lookup error: %s", resp.Status) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + lc.setValid(false) + + return fmt.Errorf("reading response failed: %w", err) + } + + var data struct { + Revoked bool `json:"revoked"` + ExpiresAt time.Time `json:"expires_at"` + } + if err := json.Unmarshal(body, &data); err != nil { + lc.setValid(false) + + return fmt.Errorf("unmarshal error: %w", err) + } + + if data.Revoked || time.Now().After(data.ExpiresAt) { + lc.setValid(false) + + return fmt.Errorf("license expired or revoked") + } + + lc.mu.Lock() + lc.valid = true + lc.lastGood = time.Now() + lc.mu.Unlock() + + log.Printf("✅ License validated. Expires: %s", data.ExpiresAt) + return nil +} + +func (lc *LicenseChecker) StartBackgroundCheck() { + go func() { + for { + time.Sleep(lc.PollInterval) + err := lc.Validate() + if err != nil { + log.Printf("⚠️ License check failed: %v", err) + } + } + }() +} + +func (lc *LicenseChecker) IsValid() bool { + lc.mu.RLock() + defer lc.mu.RUnlock() + return lc.valid +} diff --git a/main.go b/main.go index 52c871c..3f6c65c 100644 --- a/main.go +++ b/main.go @@ -34,6 +34,10 @@ func main() { logging.Error("❌ Failed to init session: %v", err) } + // 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) diff --git a/middleware/recover.go b/middleware/recover.go index 0a3a9d9..758d45e 100644 --- a/middleware/recover.go +++ b/middleware/recover.go @@ -4,7 +4,8 @@ import ( "log" "net/http" "runtime/debug" - "synlotto-website/helpers" + + templateHelpers "synlotto-website/helpers/template" ) func Recover(next http.Handler) http.Handler { @@ -13,7 +14,7 @@ func Recover(next http.Handler) http.Handler { if rec := recover(); rec != nil { log.Printf("🔥 Recovered from panic: %v\n%s", rec, debug.Stack()) - helpers.RenderError(w, r, http.StatusInternalServerError) + templateHelpers.RenderError(w, r, http.StatusInternalServerError) } }() next.ServeHTTP(w, r) diff --git a/models/config.go b/models/config.go index 91743e8..ee870ba 100644 --- a/models/config.go +++ b/models/config.go @@ -11,6 +11,11 @@ type Config struct { CSRFKey string `json:"csrfKey"` } `json:"csrf"` + License struct { + APIURL string `json:"apiUrl"` + APIKey string `json:"apiKey"` + } `json:"license"` + Session struct { AuthKeyPath string `json:"authKeyPath"` EncryptionKeyPath string `json:"encryptionKeyPath"` diff --git a/models/user.go b/models/user.go index a79c730..6e1365e 100644 --- a/models/user.go +++ b/models/user.go @@ -59,35 +59,3 @@ func GetUserByUsername(username string) *User { return &user } - -func GetUserByID(id int) *User { - row := db.QueryRow("SELECT id, username, password_hash, is_admin FROM users WHERE id = ?", id) - - var user User - err := row.Scan(&user.Id, &user.Username, &user.PasswordHash, &user.IsAdmin) - if err != nil { - if err != sql.ErrNoRows { - log.Println("DB error:", err) - } - return nil - } - - return &user -} - -func LogLoginAttempt(username string, success bool) { - _, err := db.Exec("INSERT INTO auditlog (username, success, timestamp) VALUES (?, ?, ?)", - username, boolToInt(success), time.Now().Format(time.RFC3339)) // tOdO: SHOULD BE USING UTC - if err != nil { - log.Println("❌ Failed to log login:", err) - } -} // ToDo this shouldn't be in models. Also why did i build a bool to int? just use the bool - -func boolToInt(b bool) int { - if b { - - return 1 - } - - return 0 -} diff --git a/routes/accountroutes.go b/routes/accountroutes.go index 2af5c32..feaf8fc 100644 --- a/routes/accountroutes.go +++ b/routes/accountroutes.go @@ -4,14 +4,16 @@ import ( "database/sql" "net/http" + account "synlotto-website/handlers/account" + "synlotto-website/handlers" "synlotto-website/middleware" ) func SetupAccountRoutes(mux *http.ServeMux, db *sql.DB) { - mux.HandleFunc("/login", middleware.Protected(handlers.Login)) - mux.HandleFunc("/logout", handlers.Logout) - mux.HandleFunc("/signup", middleware.Protected(handlers.Signup)) + 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/messages", middleware.Protected(handlers.MessagesInboxHandler(db))) diff --git a/routes/syndicateroutes.go b/routes/syndicateroutes.go index 9f1ad3a..8f1494b 100644 --- a/routes/syndicateroutes.go +++ b/routes/syndicateroutes.go @@ -4,21 +4,22 @@ import ( "database/sql" "net/http" - "synlotto-website/handlers" + lotterySyndicateHandlers "synlotto-website/handlers/lottery/syndicate" + "synlotto-website/middleware" ) func SetupSyndicateRoutes(mux *http.ServeMux, db *sql.DB) { - mux.HandleFunc("/syndicate", middleware.Auth(true)(handlers.ListSyndicatesHandler(db))) - mux.HandleFunc("/syndicate/create", middleware.Auth(true)(handlers.CreateSyndicateHandler(db))) - mux.HandleFunc("/syndicate/view", middleware.Auth(true)(handlers.ViewSyndicateHandler(db))) - mux.HandleFunc("/syndicate/tickets", middleware.Auth(true)(handlers.SyndicateTicketsHandler(db))) - mux.HandleFunc("/syndicate/tickets/new", middleware.Auth(true)(handlers.SyndicateLogTicketHandler(db))) - mux.HandleFunc("/syndicate/invites", middleware.Auth(true)(handlers.ViewInvitesHandler(db))) - mux.HandleFunc("/syndicate/invites/accept", middleware.Auth(true)(handlers.AcceptInviteHandler(db))) - mux.HandleFunc("/syndicate/invites/decline", middleware.Auth(true)(handlers.DeclineInviteHandler(db))) - mux.HandleFunc("/syndicate/invite/token", middleware.Auth(true)(handlers.GenerateInviteLinkHandler(db))) - mux.HandleFunc("/syndicate/invite/tokens", middleware.Auth(true)(handlers.ManageInviteTokensHandler(db))) - mux.HandleFunc("/syndicate/join", middleware.Auth(true)(handlers.JoinSyndicateWithTokenHandler(db))) + mux.HandleFunc("/syndicate", middleware.Auth(true)(lotterySyndicateHandlers.ListSyndicatesHandler(db))) + mux.HandleFunc("/syndicate/create", middleware.Auth(true)(lotterySyndicateHandlers.CreateSyndicateHandler(db))) + mux.HandleFunc("/syndicate/view", middleware.Auth(true)(lotterySyndicateHandlers.ViewSyndicateHandler(db))) + mux.HandleFunc("/syndicate/tickets", middleware.Auth(true)(lotterySyndicateHandlers.SyndicateTicketsHandler(db))) + mux.HandleFunc("/syndicate/tickets/new", middleware.Auth(true)(lotterySyndicateHandlers.SyndicateLogTicketHandler(db))) + mux.HandleFunc("/syndicate/invites", middleware.Auth(true)(lotterySyndicateHandlers.ViewInvitesHandler(db))) + mux.HandleFunc("/syndicate/invites/accept", middleware.Auth(true)(lotterySyndicateHandlers.AcceptInviteHandler(db))) + mux.HandleFunc("/syndicate/invites/decline", middleware.Auth(true)(lotterySyndicateHandlers.DeclineInviteHandler(db))) + mux.HandleFunc("/syndicate/invite/token", middleware.Auth(true)(lotterySyndicateHandlers.GenerateInviteLinkHandler(db))) + mux.HandleFunc("/syndicate/invite/tokens", middleware.Auth(true)(lotterySyndicateHandlers.ManageInviteTokensHandler(db))) + mux.HandleFunc("/syndicate/join", middleware.Auth(true)(lotterySyndicateHandlers.JoinSyndicateWithTokenHandler(db))) } diff --git a/services/tickets/ticketmatching.go b/services/tickets/ticketmatching.go index f871aee..d74616c 100644 --- a/services/tickets/ticketmatching.go +++ b/services/tickets/ticketmatching.go @@ -4,12 +4,14 @@ import ( "database/sql" "fmt" "log" - "synlotto-website/handlers" + + lotteryTicketHandlers "synlotto-website/handlers/lottery/tickets" + thunderballrules "synlotto-website/rules" + services "synlotto-website/services/draws" + "synlotto-website/helpers" "synlotto-website/matcher" "synlotto-website/models" - thunderballrules "synlotto-website/rules" - services "synlotto-website/services/draws" ) func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, error) { @@ -27,7 +29,6 @@ func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, er } defer rows.Close() - // Buffer results to avoid writing while iterating var pending []models.Ticket for rows.Next() { @@ -64,7 +65,7 @@ func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, er } draw := services.GetDrawResultForTicket(db, t.GameType, t.DrawDate) - result := handlers.MatchTicketToDraw(matchTicket, draw, thunderballrules.ThunderballPrizeRules) + result := lotteryTicketHandlers.MatchTicketToDraw(matchTicket, draw, thunderballrules.ThunderballPrizeRules) if result.MatchedDrawID == 0 { continue @@ -105,7 +106,6 @@ func UpdateMissingPrizes(db *sql.DB) error { var tickets []TicketInfo - // Step 1: Load all relevant tickets rows, err := db.Query(` SELECT id, game_type, draw_date, matched_main, matched_bonus FROM my_tickets @@ -125,7 +125,6 @@ func UpdateMissingPrizes(db *sql.DB) error { tickets = append(tickets, t) } - // Step 2: Now that the reader is closed, perform updates for _, t := range tickets { if t.GameType != "Thunderball" { continue @@ -196,7 +195,7 @@ func RefreshTicketPrizes(db *sql.DB) error { } tickets = append(tickets, t) } - rows.Close() // ✅ Release read lock before updating + rows.Close() for _, row := range tickets { matchTicket := models.MatchTicket{ diff --git a/storage/admin.go b/storage/admin.go index b968219..d33a4cb 100644 --- a/storage/admin.go +++ b/storage/admin.go @@ -5,16 +5,18 @@ import ( "log" "net/http" - "synlotto-website/helpers" + securityHelpers "synlotto-website/helpers/security" + templateHelpers "synlotto-website/helpers/template" + "synlotto-website/middleware" ) func AdminOnly(db *sql.DB, next http.HandlerFunc) http.HandlerFunc { return middleware.Auth(true)(func(w http.ResponseWriter, r *http.Request) { - userID, ok := helpers.GetCurrentUserID(r) - if !ok || !helpers.IsAdmin(db, userID) { + userID, ok := securityHelpers.GetCurrentUserID(r) + if !ok || !securityHelpers.IsAdmin(db, userID) { log.Printf("⛔️ Unauthorized admin attempt: user_id=%v, IP=%s, Path=%s", userID, r.RemoteAddr, r.URL.Path) - helpers.RenderError(w, r, http.StatusForbidden) + templateHelpers.RenderError(w, r, http.StatusForbidden) return } @@ -36,5 +38,3 @@ func AdminOnly(db *sql.DB, next http.HandlerFunc) http.HandlerFunc { next(w, r) }) } - -// ToDo need to look into audit/access log tables and consolidate diff --git a/storage/audit.go b/storage/audit.go new file mode 100644 index 0000000..e2b8dad --- /dev/null +++ b/storage/audit.go @@ -0,0 +1,22 @@ +package storage + +import ( + "net/http" + "time" + + "synlotto-website/logging" +) + +func LogLoginAttempt(r *http.Request, username string, success bool) { + ip := r.RemoteAddr + userAgent := r.UserAgent() + + _, err := db.Exec( + `INSERT INTO audit_login (username, success, ip, user_agent, timestamp) + VALUES (?, ?, ?, ?, ?)`, + username, success, ip, userAgent, time.Now().UTC(), + ) + if err != nil { + logging.Info("❌ Failed to log login:", err) + } +} diff --git a/storage/syndicate.go b/storage/syndicate.go index da5ddbf..1995dc8 100644 --- a/storage/syndicate.go +++ b/storage/syndicate.go @@ -98,16 +98,6 @@ func IsSyndicateMember(db *sql.DB, syndicateID, userID int) bool { return err == nil && count > 0 } -func GetUserByUsername(db *sql.DB, username string) *models.User { - row := db.QueryRow(`SELECT id, username, is_admin FROM users WHERE username = ?`, username) // ToDo: needs hash - var u models.User - err := row.Scan(&u.Id, &u.Username, &u.IsAdmin) - if err != nil { - return nil - } - return &u -} - func AddMemberToSyndicate(db *sql.DB, syndicateID, userID int) error { _, err := db.Exec(` INSERT INTO syndicate_members (syndicate_id, user_id, joined_at) diff --git a/storage/users.go b/storage/users.go new file mode 100644 index 0000000..d7a5955 --- /dev/null +++ b/storage/users.go @@ -0,0 +1,34 @@ +package storage + +import ( + "database/sql" + + "synlotto-website/logging" + "synlotto-website/models" +) + +func GetUserByID(db *sql.DB, id int) *models.User { + row := db.QueryRow("SELECT id, username, password_hash, is_admin FROM users WHERE id = ?", id) + + var user models.User + err := row.Scan(&user.Id, &user.Username, &user.PasswordHash, &user.IsAdmin) + if err != nil { + if err != sql.ErrNoRows { + logging.Error("DB error:", err) + } + return nil + } + + return &user +} + +func GetUserByUsername(db *sql.DB, username string) *models.User { + row := db.QueryRow(`SELECT id, username, password_hash, is_admin FROM users WHERE username = ?`, username) + + var u models.User + err := row.Scan(&u.Id, &u.Username, &u.PasswordHash, &u.IsAdmin) + if err != nil { + return nil + } + return &u +} diff --git a/templates/main/footer.html b/templates/main/footer.html new file mode 100644 index 0000000..69891a6 --- /dev/null +++ b/templates/main/footer.html @@ -0,0 +1,17 @@ +{{ define "footer" }} + +{{ end }} \ No newline at end of file diff --git a/templates/layout.html b/templates/main/layout.html similarity index 83% rename from templates/layout.html rename to templates/main/layout.html index 1d1641d..4df4b8c 100644 --- a/templates/layout.html +++ b/templates/main/layout.html @@ -1,9 +1,10 @@ {{ define "layout" }} + - SynLotto + {{ .SiteName }} @@ -12,9 +13,9 @@ {{ template "topbar" . }} - -
-
+ + +
- -
- {{ if .Flash }} - - {{ end }} - {{ template "content" . }} -
-
+ + +
+ {{ if .Flash }} + + {{ end }} + {{ template "content" . }} +
- + {{ template "footer" . }} + -{{ end }} +{{ end }} \ No newline at end of file diff --git a/templates/topbar.html b/templates/main/topbar.html similarity index 100% rename from templates/topbar.html rename to templates/main/topbar.html